Skip to content

[factory] Every Factory PR must carry a resolvable session_ref — the trajectory pointer exists but ships a placeholder #260

Description

@khaliqgant

Framing

This is not a new capability. It resumes workstreams/intent-trajectory-lineage.md — the existing doctrine for the chain human intent → logged → conversation stored → pointer written back → id travels with the work → every PR registers to it — which is status: active with owner: unassigned. Replay is a new consumer of that spine, alongside SOC-2 traceability. Per the standing rule, SOC-2 and trajectory "share substrate and must not be built twice"; replay makes three consumers of one substrate.

The goal: a PR Factory opens carries a resolvable session reference, so any teammate — not just whoever dispatched it — can open a finished PR and understand how the decisions were made.

The actual gap (verified, not assumed)

The pointer contract was ruled and demonstrated, then never applied.

The ruled format, live on relay#1476:

<!-- trajectory: work_unit_id=AgentWorkforce/relay#1476 work_unit_surface=github session_ref=unknown-session-v3b -->

Two problems with treating that as "done":

  1. session_ref is a placeholderunknown-session-v3b, not a session id. The format is proven; the population never was. Replay has nothing to resolve.
  2. It exists on exactly one PR. Checked relay#1477, #1496, #1519, #1520 and factory#246, #247, #248, #257, #258 — including all six PRs from today's Factory work. None carry a trajectory pointer at all.

So the work is not "design a linkage." It is: make session_ref carry a real, resolvable id, and make the pointer appear on every Factory PR.

What already exists — build on it, do not rebuild

  • factory#233 is merged (2026-08-11T18:30:10Z) and its code is live on origin/main at src/writeback/github.ts:115-116:

    const sessionRef = input.sessionRef ?? (process.env.RELAY_ATTEST_SESSION_ID || undefined)
    await postAttestationGrant(input.repo, sessionRef).catch(() => undefined)

    Factory already forwards a per-agent session ref into the attestation ledger. The plumbing exists; it just never reaches the PR body.

    Line numbers matter here: an earlier revision of this issue cited :248-249, read from a checkout on codex/222-routed-pr-babysitter-v2. On origin/main — which you must branch from — :248 is inside a different function. Use :115-116.

  • The gap is at one seam, already located. On origin/main, src/orchestrator/factory.ts:6387-6388:

    body: githubPullRequestBody(issue, implementer.spec.preview),
    ...(implementer.sessionRef ? { sessionRef: implementer.sessionRef } : {}),

    and the body builder's signature at :15798 is (issue: LinearIssue, preview?: PreviewReference). So implementer.sessionRef is already in hand at the call site and already reaches publishPullRequest — it is simply never passed to the function that builds the PR body. Fix that seam; do not build new carry plumbing.

  • There is a second body path. The babysitter adoption route builds a PR body independently (src/github/routed-pr-babysitter.ts, around :216-220). "Every PR Factory opens" includes adopted PRs. Fixing only the dispatch path produces a partial fix that reads as complete.

  • The SOC-2 chain shipped 2026-08-11 across relayauth#79, relay#1477, cloud#2981/#2987/#2989, factory#233.

  • work_unit_id is already ruled: never raw-borrowed from a surface. work_unit_surface names the surface — linearENG-123, githuborg/repo#number — with a synthesized id when no surface ticket exists. This is the same surface-agnosticism rule that killed the assume-Linear design. Do not relitigate.

  • Placement is already ruled (workforce#308): session continuity belongs in @agent-relay/session, not a CLI package. The replay id and the relayhistory↔relaycast join are the relay-side companion issue — not this one.

Scope — Factory side only

  1. Emit the ruled pointer on every PR Factory opens, using the exact existing format. Not a new field, not a fourth key beside it — populate session_ref.
  2. Populate session_ref with the real session id, sourced from the same value factory#233 already forwards (input.sessionRef, falling back to RELAY_ATTEST_SESSION_ID). If that value is absent, the pointer must say so explicitly rather than emitting a placeholder — unknown-session-v3b shipping to production is precisely the failure this issue exists to end.
  3. Originate the id at the prompt, not the spawn. The id should trace to where the work was expressed (a Chief dispatch or a direct instruction), because a session beginning at the implementer loses the framing that explains why the work was requested — the most valuable part for a later reader.
  4. Survive the handoffs that currently lose state:
  5. Fail loudly. A PR opened without a resolvable session_ref must log at error level and be visible in factory status. A silently missing pointer is the same failure class that made today's incident hard to diagnose.

Constraints that shape the design

Do not add workspace traffic. The relayfile workspace durable object is currently returning 429 durable_object_overloaded (#259 — Factory issues 6,674 listTree calls, one subtree enumerated 67 times). The pointer must ride existing writes. Specifically: copying a conversation slice into durable storage at PR time is the option that makes #259 worse — prefer a reference.

Relaycast conversations expire at 30 days. Verified at relaycast-cloud/packages/relaycast/src/entrypoints/cloudflare.ts:138:

pruneExpired(dbFor(env), { defaults: { messageTtlDays: 30 } })

The surrounding comment confirms per-workspace workspaces.retention overrides are honored, including an explicit null meaning keep-forever. That override is a supported first-class path and adds no workspace traffic.

The standing ruling: the durable primary reference is the ai-hist session UUID; the relaycast message id is a secondary, expiring field. Factory's pointer must therefore key on the durable id. The named failure mode from the workstream, verbatim: "a pointer to a conversation that has aged out is worse than no pointer, because it reads as coverage."

Relayhistory is per-node by construction. Any design reading from relayhistory must state what it does when the trajectory crossed nodes — a multi-node trajectory read from relayhistory alone is silently incomplete. Today's work is routinely multi-node, so this is the common case, not an edge.

A decision this issue makes explicitly

The SOC-2 principle already on the books (2026-08-08) is "reference, never payload" — an auditor should see that a session existed and belongs to a named human, not necessarily see its contents.

Replay deliberately chooses internal-teammate comprehension over auditor-minimal-disclosure: the stated goal is that a teammate reads the decisions and can question the agent. These are reconcilable — an internal teammate is not an external auditor — but they are different postures, and the pointer must not become a way for external parties to read session contents. The pointer stays a reference; access control lives at resolution time, not in the PR body.

A well-formed id is not a resolvable one

unknown-session-v3b shipped because nobody checked that the value meant anything. An id that is syntactically valid, opaque, non-placeholder — and dangling — fails identically, and is harder to detect, because it defeats grep-based verification: the grep -oE "<!-- trajectory:[^>]*-->" check below passes on a dangling id.

This is the same failure the workstream already names one layer down — "a pointer to a conversation that has aged out is worse than no pointer, because it reads as coverage."

Factory cannot fully prove resolution today, because the resolver is the relay-side companion (relay#1522) and does not exist yet. Do not block on that. Do satisfy the in-scope half:

  • The emitted value must be sourced from something that was a real session id at emission time — not synthesized, not defaulted, not coerced from an empty value into a plausible-looking string.
  • A fallback chain that quietly turns absent into String(undefined), an empty string, a freshly generated UUID with no session behind it, or the agent name is the placeholder failure wearing better clothes. That path must log at error level and surface in factory status — not emit.

Definition of done

  • Every PR Factory opens carries the ruled trajectory pointer with a real, resolvable session_ref.
  • No PR ships a placeholder, a synthesized, or a defaulted value.
  • The id traces to the originating prompt, not merely the implementer spawn.
  • It survives implementer→reviewer handoff and a mid-dispatch crash.
  • A PR opened without a resolvable ref logs at error level.
  • Cross-node behaviour is stated, not discovered later.

Verification — required, against the built CLI

Do not close this on green unit tests.

npm ci && npm run build
node bin/factory.mjs dispatch <ISSUE_KEY> --config <path>
# on the resulting PR:
gh pr view <N> --repo <org/repo> --json body --jq .body | grep -oE "<!-- trajectory:[^>]*-->"

Assert the pointer is present and session_ref is a real id, not a placeholder. Then kill Factory mid-dispatch and assert the ref survives the restart. Attach real terminal output plus the resulting PR body.

Constraints

  • Branch from origin/main. The local checkout sits on codex/222-routed-pr-babysitter-v2 with an unrelated WIP commit (b2ad725) — do not build on it and do not disturb it.
  • mergePolicy: never — open the PR and stop at review.
  • The relay-side companion issue owns relay session replay <id> and the relayhistory↔relaycast join. Do not implement the join here.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    factoryFactory automation triggerfactory:human-reviewFactory work is ready for human review.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions