164. A durable record names the session that wrote it, and relates a task only when the task is that session's
Context
Two workers share one checkout. Worker A starts a task. Worker B, another provider session,
records a decision, a checkpoint and a question while A's task is active. Today all three
records are filed under A's task and none of them names B: lib/decision.sh,
lib/checkpoint.sh and lib/question.sh take the task from the checkout's current-task
record (mj_load_current), and the ledger envelope stamps the session mj_open_session_id
resolves. That resolution tries the caller's own episode first — MJ_SESSION_KEY, then the
provider session the caller supplies (MAJORDOMUS_PROVIDER_SESSION or the variable its
lifecycle adapter declares) — and, when the caller supplies none, falls through to the
checkout's pointer, which names whichever episode was opened last. lib/common.sh calls
that last step "a guess the status output says out loud rather than a fact". Plan issue
I0701 reproduces the gap as an executable case (case 3049).
The current-task record cannot tell the workers apart either: lib/start.sh writes
owner from --owner or $USER, the same value for every worker on one machine, and no
reader compares it with the caller. The task.started ledger line, however, already
carries the session that opened the task in its envelope.
Three constraints are fixed before any choice. project.no-new-nouns: the tool keeps no
worker identity of its own. ADR 0047: a worker identity is one somebody supplied, never one
inferred, and a provider session id is a correlation id, never the episode's identity. ADR
0044 and ADR 0103: a board position is not a worker identity, and an episode is keyed by an
identity the client supplies and can reproduce. ADR 0052: a record's existence never
depends on task state — a checkpoint outside a task records no task, and is still written.
Decision
A durable record is attributed to the session that ran the command, and relates a task only when that task was opened by the same session.
- The attribution is the existing envelope key. The ledger line of
decision.recorded,question.openedandtask.checkpointcarriessessionas every ledger line does. No new field, store or identity is introduced; the provider session or worker a caller supplies stays inside the record it explains and its ledger line, as today. - No guess. When the caller supplies no identity of its own and more than one episode
is open in the checkout, the record is written with no
session— attributed to nobody rather than to whoever opened an episode last. The pointer remains an answer for status output, which says it is a guess; it is never written into a record as a fact. - The task is related only when it is the caller's. A record names the active task when
the session in that task's
task.startedline is the caller's session. Otherwise the record names no task — thetask_id-omitted shapelib/checkpoint.shalready writes outside a task — and is still written, as ADR 0052 requires. - Nothing is refused for attribution. A command that cannot attribute writes an unattributed record; it never fails for want of an identity.
Alternatives rejected
Refuse to write when the active task belongs to somebody else. It cannot detect the
case it exists for: both workers have the same owner on one machine and the current-task
record holds no session, so detecting a foreign task means keeping a worker identity in that
record — the noun project.no-new-nouns forbids. A refusal on checkpoint repeats the
outage ADR 0052 records, where a lifecycle hook's checkpoint was skipped and exited 0. And a
task started with --owner on an agent's behalf would refuse the one legitimate worker.
Require the task to be named on every write. It needs no identity, but every single
worker pays for a problem only concurrent workers have, on every decision and checkpoint.
The checkpoints a provider's lifecycle hook writes have nobody to name a task, so they are
either refused — the ADR 0052 path again — or written with no task, which makes the
requirement meaningless where most checkpoints come from. question add already refuses
without a task, and project.the-session-context-is-derived records how little these
writers are used when they add friction.
Consequences
- The implementation (the issues after I0702 in milestone M002) changes three writers and the envelope resolution, adds no store, and turns case 3049's two pinned assertions red; the change rewrites them to assert the attribution this record chooses.
- A provider that declares no session variable gets unattributed records when several episodes are open, instead of misattributed ones. That is the cost this decision accepts, and it falls on that provider's adapter, which can declare its variable.
- A single worker sees no change: its session opened the task, so its records name the task as before.
- Implementation conditions: the attribution is computed once, in the envelope, and read by
all three writers; the comparison with
task.startedreads the ledger the checkout already has; a test proves an unattributed write when two episodes are open and the caller supplies nothing.