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
- Change only a retained member's value and verify its admitted input, generation, and reference do not change.
- Remove a running member and verify that exact provider is interrupted once.
- Re-add the same key and verify a new input, generation, and reference are admitted.
- Deliver an outcome from the retired coordinate and verify it is classified as stale rather than affecting the current member.
- 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.