Guide

Keys and Incarnation Identity

Separate a stable member address from each exact execution that occupies it.

An open family uses keys to recognize retained, added, and removed members. Exact incarnation identity makes those decisions safe across interruption, retries, recovery, and late outcomes.

A key identifies one current member, not one eternal execution.

A family key is a stable member address. The engine pairs it with an incarnation identity so retained work stays put and retired work cannot return as if it were current.

1 · Address one current member

A key names membership within one family site.

The guide key identifies one member of Watchers. Its input is captured as that member is admitted, and the engine assigns the incarnation an exact generation and reference.

import { Activity, Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";

export const WatchDocument = Activity.make("WatchDocument", {
  input: Schema.Struct({ path: Schema.String }),
  success: Schema.Void,
});

const Watchers = WatchDocument.each("Watchers");

const desiredWatchers = Query.gen(function* () {
  return Object.fromEntries(
    Object.entries((yield* Workspace).documents).map(([key, path]) => [key, { path }]),
  );
});

export class Workspace extends State.Compound<Workspace>()("Workspace", {
  documents: Schema.Record(Schema.String, Schema.String).pipe(
    Schema.withConstructorDefault(Effect.succeed({ guide: "docs/guide-v1.md" })),
  ),
}) {
  static states = States.make(() => [Watching]);
}

class Watching extends State.Atomic<Watching>()("Watching") {
  static transitions = Transitions.make(this, ({ invoke }) => [invoke(Watchers, desiredWatchers)]);
}

export const WorkspaceChart = Statechart.make(Workspace);

2 · Retain the current incarnation

Keeping the key keeps the execution.

Changing guide from v1 to v2 changes the desired mapping, but guide never leaves it. Watchers preserves the admitted v1 input and the same incarnation instead of silently replacing running work.

import { Activity, Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";

export class ChangeDocumentPath extends Schema.TaggedClass<ChangeDocumentPath>()(
  "ChangeDocumentPath",
  { path: Schema.String },
) {}

export const WatchDocument = Activity.make("WatchDocument", {
  input: Schema.Struct({ path: Schema.String }),
  success: Schema.Void,
});

const Watchers = WatchDocument.each("Watchers");

const desiredWatchers = Query.gen(function* () {
  return Object.fromEntries(
    Object.entries((yield* Workspace).documents).map(([key, path]) => [key, { path }]),
  );
});

export class Workspace extends State.Compound<Workspace>()("Workspace", {
  documents: Schema.Record(Schema.String, Schema.String).pipe(
    Schema.withConstructorDefault(Effect.succeed({ guide: "docs/guide-v1.md" })),
  ),
}) {
  static states = States.make(() => [Watching]);
}

class Watching extends State.Atomic<Watching>()("Watching") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(Watchers, desiredWatchers),
    on(ChangeDocumentPath).update(Workspace, ({ event, target }) => ({
      documents: { ...target.documents, guide: event.path },
    })),
  ]);
}

export const WorkspaceChart = Statechart.make(Workspace);

3 · Retire, then admit again

Reusing a key creates a fresh incarnation.

Removing guide withdraws its current execution. Reopening guide admits v2 under the same member address, but the new generation proves this is new work—not a revival of the retired watcher.

import { Activity, Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";

export class CloseDocument extends Schema.TaggedClass<CloseDocument>()("CloseDocument", {}) {}

export class ReopenDocument extends Schema.TaggedClass<ReopenDocument>()("ReopenDocument", {
  path: Schema.String,
}) {}

export const WatchDocument = Activity.make("WatchDocument", {
  input: Schema.Struct({ path: Schema.String }),
  success: Schema.Void,
});

const Watchers = WatchDocument.each("Watchers");

const desiredWatchers = Query.gen(function* () {
  return Object.fromEntries(
    Object.entries((yield* Workspace).documents).map(([key, path]) => [key, { path }]),
  );
});

export class Workspace extends State.Compound<Workspace>()("Workspace", {
  documents: Schema.Record(Schema.String, Schema.String).pipe(
    Schema.withConstructorDefault(Effect.succeed({ guide: "docs/guide-v1.md" })),
  ),
}) {
  static states = States.make(() => [Watching]);
}

class Watching extends State.Atomic<Watching>()("Watching") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(Watchers, desiredWatchers),
    on(CloseDocument).update(Workspace, ({ target }) => ({
      documents: Object.fromEntries(
        Object.entries(target.documents).filter(([key]) => key !== "guide"),
      ),
    })),
    on(ReopenDocument).update(Workspace, ({ event, target }) => ({
      documents: { ...target.documents, guide: event.path },
    })),
  ]);
}

export const WorkspaceChart = Statechart.make(Workspace);

Keys are addresses within one site

In this family mapping, guide is a member key:

invoke(Watchers, () => ({
  guide: { path: "docs/guide-v1.md" },
}));

Its full authored address includes the owning state path and the Watchers occurrence. Another family may also have a guide member without collision. Keys need to be unique only within one family site at one time.

Choose keys from durable domain identity: a document id, partition id, worker name, or order id. Array positions and random ids usually describe a transient presentation rather than the member the domain means to retain.

Admission captures the member input

When guide first appears in desired membership, Motive admits one member and captures its decoded input. The provider receives { path: "docs/guide-v1.md" }; the published Activity entry retains that same admitted value.

This is the member's input, not a live reference back into Workspace.documents. The desired mapping may be recomputed as chart data changes, but a running execution still needs a stable claim about what work it was asked to perform.

A retained key retains its execution

The second frame changes the value behind guide without removing the key:

documents: { ...target.documents, guide: event.path }

Desired input becomes guide-v2.md. Admitted input remains guide-v1.md, and the generation does not change. No provider is interrupted or restarted.

That restraint is important. Treating any value drift as implicit replacement would make ordinary facet updates cancel work, rerun effects, and change retry identity without an authored lifecycle decision. If a changed value means a different unit of work, encode that distinction in the key or remove and add the member explicitly.

Removal retires the exact incarnation

When guide leaves desired membership, Watchers withdraws that member. A running provider is interrupted, the entry disappears from the family, and Watchers.MemberRemoved may be observed by the chart.

Withdrawal ends one exact incarnation. It does not reserve the string guide forever, and it does not mutate the retired execution into an absent state that can later be resumed.

The same key can name new work later

Reopening the document adds guide to desired membership again. The family admits another member with the new guide-v2.md input. Its member key is still guide; its generation and reference are new.

The live readout abbreviates generation as seq:ordinal. The arrow from the first generation to the second is the important fact: stable address, fresh incarnation.

This is why a key identifies the current member, not one eternal execution. Identity has two layers:

  • the authored site and member key say which domain role an outcome addresses;
  • the engine-owned generation and reference say which admitted execution currently owns that role.

Exact coordinates fence late outcomes

An Activity settlement carries the state path, occurrence, member key, arming, generation, reference, and attempt that produced it. The engine admits the settlement only when that coordinate still matches the current member.

A late success from the retired guide watcher still names guide, but it carries the old generation and reference. It is stale and cannot complete, fail, or overwrite the newly admitted watcher. Reusing a human-readable key therefore does not reopen authority for old work.

Application code should not construct these coordinates. Motive creates them when work is admitted and passes them through its runtime protocol. The modeling responsibility is to supply meaningful, stable member keys and explicit replacement boundaries.

Test address and incarnation separately

  1. Change only a retained member's value and verify its admitted input, generation, and reference do not change.
  2. Remove a running member and verify that exact provider is interrupted once.
  3. Re-add the same key and verify a new input, generation, and reference are admitted.
  4. Deliver an outcome from the retired coordinate and verify it is classified as stale rather than affecting the current member.
  5. Use the same key in two separately named family sites and verify their full addresses remain independent.

Next, membership epochs explain when a whole cohort should keep one captured identity and when the model should deliberately advance to another.