Skip to content

waiting_until means 'expected to resume', and the human handoff's deadline is a give-up — one field, two opposite actions #596

Closed

Description

@serge-ivo

A park's until conflates "expected to resume" with "about to give up"

4d4f52f (#591) made waiting_until real: an engine_limit park now records the instant it expects to resume, and work-report.ts:127 renders it as "expected to resume in …".

The human handoff deliberately does not set it, and that was the right call — its 15-minute deadline is a give-up, not a resume. Rendering a give-up time under "expected to resume" would tell an owner to wait for something that is about to stop waiting for them.

So the field is correct today only because one of its two producers abstains. That is a gap held open by omission, not by design.

The promise is now half-true, in two places

Why this misleads

The two deadlines demand opposite actions. "Resuming at 16:00" means do nothing. "Giving up at 16:00" means intervene now. A single field rendered with a single verb cannot carry both, and the owner cannot tell which they are looking at.

Acceptance criteria

  1. A deadline that is a give-up is distinguishable from one that is a resume — a second field, or a reason-aware renderer. State which and why.
  2. The human handoff's 15-minute deadline becomes visible. It is currently knowable only by not being there.
  3. composition.ts:255 and RUN_HEALTH_LEGEND state what is true for every park reason, not the best case.
  4. A guard asserts every park reason's deadline is rendered with the verb matching its kind, denominator over all park reasons per ADR 0002.
  5. Red demonstrated: a human-handoff deadline must not render as "expected to resume".

Verified vs inferred

  • Verified: work-report.ts:127's clause; that the human path passes no until; both legend strings; coding.ts:170.
  • Inferred: nothing. The abstention is deliberate and was reported as such by the agent that implemented it.

Activity

  1. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    Shipped in 41cebd1 on main. CI green, API + MCP deploys green.

    AC1 — the decision: derive the verb from the park reason, do not store a second field

    workers/api/src/lib/work-report.ts now holds a PARKS table keyed by every RunWaitReason,
    each entry carrying what the park waits FOR and which kind of deadline it has
    (resume | give_up), plus a DEADLINE_CLAUSE table with one clause per kind. The docblock
    records the alternatives and why they lost:

    RunWaitReason became RUN_WAIT_REASONS, an as const array (agent-loop-store.ts), for the
    reason RUN_HEALTH_STATES is one.

    AC2 — the handoff's 15 minutes are visible, in two places

    • Chat, at the moment the handoff opens: "…take over; the run resumes where it stopped. If
      nobody does within 15 minutes it stops and reports that it is waiting on you."
    • The record: waitForHuman now passes until to deps.tick, so waiting_until carries the
      give-up instant. It is computed from the polls remaining, so every tick names the same
      absolute moment and it survives a Workflow replay — a deadline captured before the loop would
      have slid a whole wait forward. The rendered note reads:
      …it is waiting for YOU to answer a handoff, and it GIVES UP in 12m if nobody does — that is a deadline for YOU, not a resume time.

    The 15 minutes is now one constant (HANDOFF_GIVE_UP_MS) behind the announcement, the expiry
    sentence and the published instant.

    AC3 — the legends

    RUN_HEALTH_LEGEND now states what is true for every park reason: the note says what for and,
    when the end is knowable, when the wait runs out and what running out means — a usage-limit
    park resumes then, a human handoff gives up then. composition.ts records the same, and warns
    against re-deriving a resume time from waitingUntil in a caller.

    The coding.ts:170 asymmetry named in the issue is already gone: since #588 both tools publish the
    same rendered runHealthSentence().

    One residue, deliberately left: workers/mcp/src/state-vocabulary.ts's waiting gloss still
    says the note "gives a resume time only when one is knowable". That stays true — a resume
    time is still given only for the parks that resume — but it no longer mentions that a park can also
    state when it gives up. That file is owned by the in-flight #600/#603 lane and was off limits to
    this change. The one-line replacement, for whoever gets it:

    deliberately parked — waitNote says what for and, when the end is knowable, when the wait runs
    out AND what running out means: a usage-limit park RESUMES then, a human handoff GIVES UP then.
    Read the verb, because one park is waiting for a PERSON and its clock runs against them.

    AC4 / AC5 — the guard, red demonstrated

    run-health-readers.test.ts walks RUN_WAIT_REASONS at run time and asserts each reason's
    deadline is rendered with the verb matching its kind, that both kinds are actually exercised (a
    table where every reason was a resume would otherwise satisfy half the rule and pass), and that
    an unrecognised reason gets no deadline clause. Denominator printed per ADR 0002 G2.

    The previous denominator was Record<RunWaitReason, true> in a test file — inert:
    workers/api/tsconfig.json excludes src/**/*.test.ts and vitest transpiles without checking, so
    it compiled nowhere and could never have gone red. A fourth park reason would have walked straight
    past the one guard whose job is to count them.

    G4, on the pre-change tree (renderer and writer both reverted, three guards flip):

    × renders EVERY park reason's deadline with the verb matching its kind
      → `human` is a give-up and must NOT be rendered as a resume:
        'WAITING, not stalled and not working — it is waiting for YOU to answer a handoff,
         expected to resume in 12m. No instruction has advanced since it parked…'
    × a human handoff's deadline is a GIVE-UP, never 'expected to resume' (#596)
    × a human handoff ticks WITH its give-up instant, which is the deadline the owner can beat
    

    The third drives the real resolvePause, so it fails on the production call site rather than on a
    hand-passed argument.

    Verified / not verified

    • Verified: pnpm typecheck (11 projects), full pnpm test (9,032), biome, all eight
      scripts/check-*.mjs gates incl. check-surface-lock --require-history, docs:drift, and CI
      green on 41cebd1. No MCP tool description changed (only comments), so no surface-lock bump.
    • Not run: pnpm test:e2e — nothing under store/, agents/coder/web was touched.
    • Not observed live: no run is parked on a human handoff right now, so the new give-up sentence has
      not been read off a production row. It is pinned by the unit guards above, and the next handoff
      will carry it.

    #601

    Unblocked. waiting_until now has one meaning a reference page can state: the instant this
    park's clock runs out
    , with the kind of ending entailed by waiting_reason and rendered from
    work-report.ts's PARKS — engine_limit and platform_interrupt resume at it, human gives up
    at it. There is no second field to document and no case where the same value means two things.

  2. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    Leaving this open for one line: the state-vocabulary.ts gloss above (AC3's MCP half). Everything else — AC1, AC2, AC4, AC5 and the API-side legend — is on main in 41cebd1. That file belongs to the #600/#603 lane; the replacement sentence is in the comment above, and the claim it makes today is incomplete rather than false, so nothing is misreporting in the meantime.

  3. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    Live check (read-only, via the MCP-reading agent), for the record: nothing is parked on the estate right now — zero active coding sessions across all nine coding-capable instances, runner online (RLs-MacBook-Air.local, 0.4.51, last seen 12:41), so there is no waiting run whose waitNote could be read back from production. The closest live evidence is two engine_limit parks that ran out today (boards bd43f4de-… and e4d2d031-…, both "the coding CLI is still reporting its own usage limit… resets Aug 17 at 4pm") — closed runs, so their waitNote is null by design (work-report.ts: if (run.status !== \"running\" || !run.waitingReason) return null).

    So the give-up sentence is pinned by the guards and by the deployed code path, and has not been observed on a live row. The next handoff will carry it; I did not start a run to manufacture one.

  4. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    ContributorAuthor

    Audit verdict: delivered — closing. 41cebd15 + b4d6ea3b.

    AC evidence
    1. a give-up is distinguishable from a resume, and which/why is stated lib/work-report.ts:214 — PARKS, "every park reason, with what it waits for and which KIND of deadline it has (#596)". The kind is derived from the park reason, not stored as a second column, with the reasoning at :280 and at agent-loop-store.ts:110: "the thing that renders it, so the verb can never be chosen by a reader that guessed"
    2. the human handoff's 15-minute deadline becomes visible lib/coding-pause.ts:152 — "The deadline is said OUT LOUD (#596 AC2)". Test: coding-pause.test.ts:194 "tells the owner BY WHEN, because a handoff deadline they cannot see is one they cannot beat"
    3. composition.ts and RUN_HEALTH_LEGEND state what is true for every park work-report.ts:129 now reads "when the end is knowable … a usage-limit park RESUMES then, a human handoff GIVES UP then. Read the verb before you decide it needs nothing". workers/mcp/src/state-vocabulary.ts:185 records the same correction on the MCP side
    4. a guard over all park reasons, denominator per ADR 0002 lib/run-health-readers.test.ts:213 — "Every park's deadline is rendered with the verb matching its KIND (#596 AC4)"; :287 — "No legend promises a resume time for every park". RunWaitReason is an array as well as a union specifically so the guard can count it (agent-loop-store.ts:57)
    5. red demonstrated work-report.test.ts:324 — "a human handoff's deadline is a GIVE-UP, never 'expected to resume'"

    The issue's framing — "the field is correct today only because one of its two producers abstains" — is what the fix inverted: the human handoff now publishes its deadline, and the reason it is a different kind of deadline is carried in the data rather than in the abstention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions