Skip to content

US-45.9: an interactive session implements in a change thread, a runner reviews (#917) - #919

Merged
mkreyman merged 6 commits into
masterfrom
feature/us-45.9-interactive-threads
Sep 28, 2026
Merged

mkreyman merged 6 commits into
masterfrom
feature/us-45.9-interactive-threads

Conversation

@mkreyman

Copy link
Copy Markdown
Owner

Closes #917. Implements US-45.9 (#918).

What

Any claim a session made itself used to read as pr mode, and nothing could move its stage. Now it can be a change thread:

  • Route bound at claim. An interactive claim writes its route to the new claim_routes table in the claim's transaction: the source's mode and base branch, plus a loop/ branch in thread mode. DispatchLedger.route_rows_query/0 unions those rows with the ledger's accepted implement rows. So the merge gate, the executor, the thread merge sweep and the thread page's diff all read it through the one derivation they already share, and changing a source later affects only later claims.
  • Which claims. Placement passes an explicit placement: marker and records no interactive route. A bulk claim is interactive. A thread route is recorded only for a story triage queued, because Gate A refuses anything untriaged. Any other claim records a pr route.
  • Stage. The claim moves queued → claimed as a control transition after it commits. It can't run inside the claim's transaction, because advance/4 reads the story on another connection.
  • Claimant's stage reports. POST /api/v1/stories/:id/stage/transitions lets the claimant report the rest of its stages. The body goes through the runner channel's own cast_stage/1, so only the transitions and effects a runner may report get through. The claimant needs a live lease and an interactive thread route. The first report from claimed also makes the claim's own move if that didn't land, as with a bulk claim.
  • Review separation. Review placement no longer refuses a claim made without a dispatch. Its reviewer is separated by agent identity, as the custody gates separate a pre-dispatch story. The reviewer is still a runner (Epic 45 dropped key-holding reviewers after US-45.1: the change-thread ledger #901/US-45.3: review dispatch, checkpoint-bound findings and a server-side round ceiling #905).
  • MCP 2.109.0. Adds thread_stage_report. The claim_story, renew_story_claim and thread_request_review descriptions now explain the interactive thread.
  • Docs. CHANGELOG, OpenAPI, the TierCapabilities surface, the route snapshot, and three chain-of-custody skill citations that moved.

Checks

  • Commit gate: 11951 tests, 0 failures. One earlier gate run had 6 failures that passed on re-run. I didn't capture their names, and the next full run was clean.
  • Mutations: each exit 0. They cover:
    • the placement marker, both in claim_story and in Placement (the latter against the placement suite);
    • thread routes only for queued stories;
    • the union;
    • the post-commit claimed move;
    • the bulk route;
    • the claimant, lease and thread-only checks;
    • the backstop;
    • effects passed through;
    • a dispatchless claim being reviewable.
  • Removed: the epoch pre-check survived its mutation (Stages.advance/4 is the fence), so I deleted it.

Still external

The review itself needs runner contract adoption: loopctl-runner#55 and #56.

The review gate is running.

…unner reviews it (#917)

A claim a session makes itself had no route and no stage writer, so every reader saw it as
pr and nothing moved its stage. Now:

- claim_routes records an interactive claim's route (the source's mode and base branch, and a
  loop/ branch in thread mode) in the claim's transaction. DispatchLedger.route_rows_query
  unions it with the ledger's accepted implement rows, so the merge gate, the executor, the
  thread merge sweep and the thread page's diff read it through the one derivation they
  already share. A placement claims with an explicit placement marker and records none; a
  bulk claim is interactive. A thread route is recorded only for a story triage queued (Gate
  A refuses any other), else a pr route.
- The claim moves queued -> claimed as control after it commits (the transition runs on
  another connection), and POST /api/v1/stories/:id/stage/transitions lets the claimant report
  the rest: the body goes through the runner channel's own cast_stage, so only the transitions
  and effects a runner may report pass, under the claimant's live lease, on an interactive
  thread route. Its first report from claimed makes the claim's move when that did not land.
- Review placement no longer refuses a claim made without a dispatch: its separation is by
  agent identity (the claimant, and any agent that recorded a checkpoint of the thread), as
  the custody gates separate a pre-dispatch story.
- MCP 2.109.0: thread_stage_report; claim_story, renew_story_claim and
  thread_request_review describe the interactive thread.

Mutations, each with bin/mutate.sh, all exit 0: the placement marker in claim_story and in
Placement, queued-only thread routes, the union, the post-commit claimed move, the bulk
route, the claimant check, the lease check, thread-only routes, the backstop move, effects
passed through, and a dispatchless claim being reviewable. The epoch pre-check survived its
mutation because Stages.advance is the epoch fence, so it was removed.
…ontain

refute log =~ "0.123" failed CI on #919 because the log line leads with the process id,
and pid <0.123751.0> contains 0.123. The refutes now look for "[0.123", the literal as it
appears in the query. Mutation: logging inspect(error) in DBErrorLogger makes the test
fail (bin/mutate.sh exit 0).
…erge

- The endpoint accepted every runner-reportable transition, ci -> merged and merged ->
  deployed included, so the implementing session could record a merge the gate never
  allowed and no executor made. StageMachine.claimant_reportable?/3 is the runner set
  short of entering merged or deployed and of leaving merged; in thread mode loopctl
  records the merge itself.
- The lease check re-implemented part of Claimant.live?/2 and missed a requested review;
  the endpoint now uses Claimant.live?/2, the definition the checkpoint and fix paths use.
- The caller's lineage comes from Dispatches.lineage_for_api_key/2, not a hand copy.
- InteractiveClaims.enter_claimed/3 fetches actor_role and actor_lineage instead of
  defaulting them, so a caller that forgot to resolve a lineage is refused.
- The route insert is an upsert on its unique key: a leftover row at the claim's epoch no
  longer aborts the claim's (or a bulk claim's) transaction.
- The thread branch takes the prefixes the tenant's live runners declare, falling back to
  loop/, and the claim response names the route (mode, base_branch, branch) to push to.
- OpenAPI now documents 404, 409 not_claimant and 503; the MCP README no longer lists the
  removed implementer_dispatch_required.
- Tests: the merge refused to the claimant, a review-requested claim not live, another
  tenant's agent 404, the leftover-row upsert, runner-declared prefixes, the route in the
  claim response, and the lineage guard.

Mutations, each with bin/mutate.sh, all exit 0: the seven above, plus every mutation the
first commit cites, re-run.
…the window

capture_log also catches what a concurrent async test logs, and a WebhookDeliveryWorker
refusal reddened the "logs nothing" test on #919 CI. It now refutes the monitor's own
custody_halt_config_invalid code. Mutation: making the valid path log that warning fails
the test (bin/mutate.sh exit 0).
…a resent report, PRD branch

- Reviewer separation failed OPEN when the implementer dispatch's lineage read empty; it now
  refuses unresolvable_dispatch_lineage, as the custody gates do.
- A resent stage report (lost response) answers the row with replayed: true; a report the
  row disagrees with is 409 stale_stage naming the row's stage and recorded effects.
- The claimant check is Claimant.check/3, the shared rule, not a copy.
- The thread branch is the PRD's loop/<story-id> again. Round 1 took it from connected
  runners' declared prefixes, which made it depend on who was online and could silently
  downgrade a thread claim; a runner's prefix constrains that machine, not a session. A
  claim with no conforming branch now logs why it is pr.
- A bulk claim reads its projects' sources once under its locks
  (InteractiveClaims.sources_by_project/2), and a route fault raises and rolls the batch back
  instead of reporting one story's error after its claim was written.
- The claim returns its route on a virtual Story field instead of querying it again; the
  claim response schema documents route; a claim with no resolved lineage skips the claimed
  move (logged) rather than defaulting one, and the claimant's first report makes it.
- Docs: the tool description, README, CHANGELOG, AC-45.9.2 and the controller moduledoc say
  the claimant reports up to ci; one duplicate Added heading folded.

Mutations, each with bin/mutate.sh, all exit 0 (20): unresolvable lineage, replay, recorded
effects, Claimant.check wiring, batch sources, the route on the claim, the PRD prefix, no
defaulted lineage, and every mutation the earlier commits cite, re-run.
…imant reports the deploy

- An interactive thread claim no longer moves its stage row at the claim. The claimant's
  first stage report enters claimed, so a claim released before any reported work requeues
  nothing and spends no retry attempt, and no chained write runs after the claim commits.
- The claimant keeps the runner's edges out of merged (deployed with release_id, the session
  escalation): nothing else writes them for a claim with no runner. Entering merged and
  merge_refused stay refused.
- effect_conflict is answered as effect_conflict with the recorded effects, never folded
  into stale_stage; a stale report carries the row's stage and effects.
- The claimant's transitions go through RunnerStages.answering_broken_chain, so a hash
  violation answers audit_chain_append_failed.
- A runner's judgement whose implementer lineage is unreadable is refused
  reviewer_not_separate rather than internal_error.
- Docs: the review endpoint's 409 unresolvable_dispatch_lineage (OpenAPI, MCP tool, README),
  dispatch_route's doc names the interactive route, the test file states why it cannot be
  async.

Mutations (bin/mutate.sh, all exit 0): the review-code mapping, the claimant set's merged
and merge_refused exclusions and its deployed allowance, the effect_conflict answer, the
replay's effect agreement, the broken-chain wrap, and the first-report claimed move.
@mkreyman
mkreyman enabled auto-merge (squash) September 28, 2026 04:37
@mkreyman
mkreyman merged commit a69aacb into master Sep 28, 2026
17 checks passed
@mkreyman
mkreyman deleted the feature/us-45.9-interactive-threads branch September 28, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Change threads are runner-only: let interactive sessions implement and be reviewed in a thread

1 participant