0080 — Owed items: proof closes, a stall pages

1. The decision

  1. One ledger, one chase. An owed item is a row in the owed_items table, in the same file as the ask ledger (~/.claude/smith-state/inbound-asks.db). The row holds: kind, key, opened, due, owed-to, link, what happened, and the close proof. The 5-minute owed-ask sweep in the Slack socket also sweeps owed items. No kind gets its own checker, cron or channel.
  2. Only proof closes. Each kind names what counts as proof. Proof is something outside the agent's own say-so: the PR merged, a decision row answered, the check ran green, a person's mute, a later DONE line for the same subject, or a person closing it by hand (python -m agent_harness.owed_items close KEY --by NAME --proof TEXT). A FAILED, EXPIRED or "escalated" line opens or keeps an item. It never closes one.
  3. A stall pages once. When an item is still open at its due time, the sweep posts ONE top-level message in #alerts (C0BGTNSP04C). It tags Gera, and Fede too when the item is owed to him. It links the record, or says in plain words why there is no link. It names the one thing blocking, when the kind can read it. The page is claimed in the row before it posts, as owed_asks.claim_page does, so racing sweeps and restarts never page twice. A post that fails gives the claim back.
  4. Recorded at the boundary, not by each workflow. Every durable workflow's terminal line goes through one transport (slack_send_activity, via workflows.closure.terminal_post). An EXPIRED terminal whose label is an owed kind opens its item there. A later DONE terminal for the same label and subject closes it. That one hook covers the PR drive, the prompt responder, tasks, operator tasks and decisions. No workflow code changes, so no Temporal replay risk.

2. The kinds and their bounds

The bound is how long a person can reasonably wait AFTER the system has said it stopped looking. The system has already spent its own budget (36 h for a drive, the responder's lifetime, 3 red nights), so each bound is short.

Kind Opened by Proof that closes it Pages when
pr-drive-expired ⏱ PR-drive · repo#N — EXPIRED (36 h lifetime or wake budget) the PR is merged or closed on GitHub, or a later PR-drive · repo#N — DONE still open 6 h after the EXPIRED line. The page names the one blocker GitHub reports: a hold label, changes requested, a red check, conflicts, or a draft
prompt-no-row ⏱ Prompt · <session> — EXPIRED whose detail says N with no decision row a decision row for that session, raised after the escalation, now answered still open 2 h after the EXPIRED line
red-check automation_run's chronic alarm, the night a job's streak reaches 3 (failure_streaks.CHRONIC_AFTER_NIGHTS) the job runs green; or a person mutes its alert source (alert_ignore); or the job is retired (it is gone from the nightly queue while the queue still ran) at once (the 3 nights are the bound), unless an open fix PR exists for its Alert ID. Then it pages if the job is still red 24 h after that PR merges
task-expired ⏱ Task …, ⏱ Operator task … or ⏱ Decision … — EXPIRED a later DONE terminal for the same subject, or a person closes it still open 24 h after the EXPIRED line
handoff-claimed a seat accepts a bridge HANDOFF ("Smith takes it — handoff X is mine") the claim is marked done with a link; or it is released to a NAMED taker, and that taker's own claim on the item exists (it accepted) not done 72 h after the claim — superseded by the 2026-11-01 amendment below

Handing on a claimed item. (Superseded by the 2026-11-01 amendment below.) A seat that holds a live claim cannot hand that item to another seat without a reason. The outgoing HANDOFF's why must say it is a release, and name the taker. The item stays owed by the claimer until the taker's claim lands. The flip-flop on 2026-10-31/11-01 (Smith took handoff 003-voice-sim-sunset-2026-10-31, then handed it to Trinity the next morning) is exactly this case.

Retired overnight tests close their items. The retire-overnight-tests lane (session b28682e0, per Fede) is removing some nightly jobs. A job gone from the nightly queue while the queue still ran is "retired", and that closes its red-check item. This is told apart from a worker outage (the queue did not run at all), which failure_streaks bridges and which keeps the item open.

3. The survey: every place with this shape

Searched agent-harness for EXPIRED, expired, Slack only, nobody is watching, lifetime reached, handed to, drive expired and owns it now. Counts are from the outbound ledger, 14 days to 2026-11-01.

Where What it says Verdict
workflows/pr_drive.py (EXPIRED, lines 2385, 2411, 3023) "still open after 36h — drive expired. The open-PR reconciler owns it now"; "used its N-turn budget" owed, pr-drive-expired (29 in 14 days)
workflows/prompt_responder.py (_finish, line 1316; the Slack-only warning, line 1061) "N escalated (N with no decision row — Slack only)"; "escalation stayed Slack-only" owed, prompt-no-row (59 of 131 Prompt EXPIRED lines carry a no-row count)
automation_run.py chronic alarm (failure_streaks.chronic_banner) "has now failed N nights in a row … needs an owner, or muted deliberately" owed, red-check (Morpheus 41 nights, maintenance-eval 89+, spine-health 49)
workflows/task_goal.py (922, 1214, 1392), workflows/operator_task.py (3230, 3596, 4486, 4500, 4558, 5304) "lifetime reached with the task …", "row is unfinished and nobody is watching it" owed, task-expired (9 in 14 days)
workflows/decision_block.py (924) a decision that expired with no answer owed, task-expired
bridge handoff intake (slack_socket BRIDGE HANDOFF, bridge_handoff.handoff) "Smith takes it — … is mine", then a HANDOFF of the same work owed, handoff-claimed (rules and bounds: the 2026-11-01 amendment)
pr_drive Deploy EXPIRED (20 in 14 days) the deploy watch stopped before the worker served the sha not owed to a person. The deploy watcher's own alarm and red-main own it
workflows/task_watch.py (339), workflows/eval_heal.py (lock_timeout), workflows/job_run.py (timeout) a watch or a machine job ran out of time not owed to a person. A nightly job that keeps failing becomes red-check
workflows/approval.py, closure.py docstrings ("nobody is watching") comments, not posts nothing to record

4. Why this shape

5. Consequences

6. Slices

Part What Repo
0 this ADR, and the smith-owed-asks catalog entry extended agentflow-relay
1 the owed_items ledger, the kinds and bounds, the EXPIRED/DONE recorder at the transport, the owed/close CLI agent-harness
2 the red-check and prompt-no-row recorders and their proof reads agent-harness
3 the sweep: proof reads, one page per item, in the owed-ask tick (after #1793 lands) agent-harness
4 handoff-claimed: the claim on acceptance, and the release rule on re-handing (re-sliced as 4, 4b, 4c by the 2026-11-01 amendment) agent-harness

Amendment 2026-11-01 — handoffs: the accepter owns it, the origin thread hears, and customer items move in minutes

Decider: Gera, 2026-11-01, on two cases the same day:

This amendment replaces the handoff-claimed row of §2 and the "Handing on a claimed item" paragraph.

  1. The seat that ACCEPTS a handoff owns it, even if it relays the work to a session. A relay is not a release. The owed item is opened at intake on the receiving seat (slack_socket._handle_sibling), keyed on bridge_claims.claim_key(lane, item) and owed to the sending seat's human. Only two things close it:

    • the claim marked done with a link;
    • a release to a named SEAT whose own claim on the item exists.

    A session the accepting seat hands the work to is never a taker.

  2. The origin thread hears the acceptance and the outcome. Each gets one plain line mirrored back into the ORIGINAL thread the alert was in, which is where the human looks. The line comes from a mechanical emitter the slack_post one-voice rule allows (a machine line, not a second brain voice). For example: "Trinity accepted this at 15:56" and "Trinity finished it: ". "Handed to Trinity" is not the last word in a thread any more.

  3. Bounds, by who is waiting:

    item first bound then
    customer-facing (a prospect, resident or lead is waiting) 30 min to proof of done; then Smith pings the accepter on the bridge, once 30 min more, then one page to Gera AND Fede
    internal 4 h one page to Gera (and Fede when it is his)

    These replace the 72 h bound. Whether an item is customer-facing comes from the HANDOFF's lane and item: a lead, prospect, resident or tenant word, or a customer alert source. An unknown item counts as customer-facing, so it fails toward the faster page.

  4. A handoff still needs a reason beyond "their area" (bridge_handoff.ownership_only, part 4). A code bug the alert engine found is the finding seat's to fix.

Slices:

Part What
4 the intake owed item, and the reason rule (agent-harness #1807)
4b the reader: done or seat-release proof, and a relay is never proof; the bridge ping at the first bound and the page at the second; customer and internal bounds
4c the acceptance and outcome lines in the origin thread

Clarifications (2026-11-02, from the amendment's review):

Where the item lives (2026-11-02, part 4b, agent-harness #1820). "The accepter owns it" is the rule about responsibility. The record of it is kept by the SENDING seat:

Gera approved this on 2026-11-02. Smith posted "I started a session … it hasn't printed its link yet … I'll post the link once it prints" four times in two days. It never followed up, even when the session ran and opened a PR (propflowai #24620 from lambda-version-prune). The launcher's lookup was walking the same transcript tree 525 times, so the link never printed in time. local-bin#736 fixes that. This amendment adds the follow-through as one more kind on this ledger, not a second tracker.

Kind Opened by Proof that closes it Bound
session-link Smith's delivered post says it started a session AND that its link is not there yet (outbound_ledger.record → session_link_owed.note_post), keyed on the slug of the turn's own launcher call a later Smith message in that thread carrying the session's id or its PR; or Smith's own one line in the thread: the session link once its id is readable, or, at the bound, the honest line that it never started and what was checked (tmux stamp, task file, recorded PR) 30 min

Why 30 minutes. A Driver takes its kickoff 7 to 47 seconds after its tmux is created (measured on the 33 live Drivers, 2026-11-02), and the fixed launcher prints its link within about a minute. Thirty minutes with no id to read means it never started. A person waiting in the thread should hear that from Smith in the thread, not as a page.

How this kind differs from the other rows. Its normal ending is a line in the asking thread, not a page in #alerts. Each line is claimed before it is posted, under the same rule as the handoff acceptance lines. A line that fails to post is not proof. The item stays open, and an item past its bound with no line pages like any other. Its proof is re-read every 5 minutes instead of hourly, so the link is posted the tick after it becomes readable.

Slice: agent-harness #1854.

Amendment 2026-11-04: a stall that is Gera's alone pages his DM

This amends where §1.3's page posts, for one class of item. Everything else in §1.3 holds: one page per item, claimed before it posts, a link or a plain reason for none, the one blocker named. In #alerts the page still tags Gera, and Fede too when it is his; a page in Gera's DM tags nobody, since a DM needs no mention.

Why. Fede asked for a retro on #alerts noise on 2026-11-04 (dock alerts-noise-retro). In the outbound ledger from 2026-11-03T08:00Z to 2026-11-04T14:00Z, 11 of the 41 top-level #alerts posts were owed-item pages. 5 of the 11 were prompts with no decision row, owed to nobody but Gera. Gera ruled on 2026-10-14 that per-event machine pages go to his DM, not the team channels, and the gera-dm purpose line in propflowai config/channels.toml says the same.

The rule (agent-harness owed_chase.page_channel):

Slice: agent-harness #1921.