Skip to content

[Frontier] [WORK15-02] Keep owner stall replies that wait to resume a duty durable across restart #367

Closed

Description

@chayapats

[Frontier] [WORK15-02] Keep owner stall replies that wait to resume a duty durable across restart

Parent

S15 — #316. Canonical spec: docs/specs/S15-conversational-autonomy.md.

Model tier

Frontier: restart reconciliation of a pending authority-bearing resume (the owner's reply must still be the authorization, exactly once, after the pause, in scope).

Gap found (review follow-up from PR #363, verified on main c5f476e)

src/s15/web/handler.ts keeps owner stall replies that wait for their own CHAT completion before work.resume in process memory (const resumes = new Map<string, { dutyId; conversationId }>() and resumeRefusals). settleResumes runs only when the owner renders a page. If the controller restarts after the owner sent "try the recommendation" but before that request completes, the binding is lost and the paused responsibility never resumes, even though the owner asked for it. Refusal reasons are lost the same way. Telegram (CHAN15-01) needs the same binding outside the Web handler.

Context to read

AGENTS.md; CONTEXT.md; docs/specs/S15-conversational-autonomy.md (S15-D22, S15-AC15); src/s15/web/handler.ts (resumes, resumeRefusals, settleResumes, /s15/forms/message duty handling); src/s15/web/ports.ts (s15DutiesPort); src/s15/work/** (resume, responsibility projection, pause checks); src/s15/conversation/** (request rows and states); src/s15/composition.ts (drain).

Deliverable

A protected, durable "resume after this owner request completes" binding owned by the WORK/conversation domain, used by the Web handler now and by Telegram later. It settles exactly once after the named CHAT request reaches a terminal state, including after a restart and without needing a page view, and keeps refusal reasons visible.

Implementation steps

  1. Add a versioned table in the protected S15 journal for pending resume bindings: request ID (unique), duty ID, conversation ID, owner identity/generation snapshot, created time, state (pending/resumed/refused), refusal code.
  2. Record the binding in the same protected step that accepts the owner's stall reply (no window where the request exists without its binding).
  3. Settle in the service drain loop: for each pending binding whose request is terminal, call the existing WORK resume checks (arrived after the pause, in scope, authorized). Decide and document how the authorization is carried without a live Web session: the authenticated owner request that created the binding is the authorization; a stale owner generation or revoked scope refuses.
  4. Remove the in-memory maps from the Web handler; render refusal reasons from the durable table.
  5. Expose a small port (e.g. bindResume(actor, {requestId, dutyId, conversationId}) and a projection) that CHAN15-01 reuses. Bounded retention for settled rows.

Acceptance criteria

  • A stall reply sent before a restart resumes the duty after restart once its request completes, without any page view.
  • Resume happens at most once per binding; a second settlement pass is a no-op.
  • A request that ends not done, a stale owner generation or a revoked scope records a refusal that the Web UI shows.
  • The Web handler holds no resume state in memory.

Tests and evidence

  1. Bind → restart service → request completes → drain resumes the duty once.
  2. Request ends refused/failed → binding refused with execution_not_authorized; UI shows it.
  3. Owner generation changed before settlement → refused.
  4. Two drains concurrently → one resume.
  5. Existing web/work suites pass unchanged.

Requirements

FR-04/14/15/18/26/28; NFR-05/08/10. Acceptance: S15-AC15.

Blocked by

None. Bundled in one PR with CHAN15-01 (#368).

Contracts used/delivered

Uses WORK resume and conversation request rows. Delivers a durable resume binding port with TSDoc.

Out of scope

Changing pause/stall detection, budgets or recommendations.

Do not

Do not resume from model output or quoted text; do not resume twice; do not add AI attribution; do not merge the PR yourself.

Definition of done

bun install --frozen-lockfile, bun run typecheck, bun run test, git diff --check pass on the final HEAD. PR body includes Closes #367. No merge by the implementer.

Activity

  1. added
    enhancementNew feature or request
    ready-for-agentImplementation-ready; start only when required blockers and ownership checks pass.
    on Nov 3, 2026
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

    enhancementNew feature or requestready-for-agentImplementation-ready; start only when required blockers and ownership checks pass.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions