Skip to content

fix: reconcile stopped steer delivery durably - #684

Draft
Dante-dan wants to merge 4 commits into
LodyAI:mainfrom
Dante-dan:fix/666-reconcile-stopped-steer
Draft

Dante-dan wants to merge 4 commits into
LodyAI:mainfrom
Dante-dan:fix/666-reconcile-stopped-steer

Conversation

@Dante-dan

Copy link
Copy Markdown

Related issue

Closes #666

Problem / pressure

Stopping a turn while an acknowledged ACP steer was still awaiting its application verdict could leave the steer waiter and pending_apply history state behind. Treating the timeout as non-delivery would be unsafe because the provider might already have accepted the guidance; leaving only process-local cleanup was also unsafe because a daemon restart or late history replica could dispatch the same steer again.

Summary

  • Separate raw steer delivery from the locally abortable application wait and retain it in normal ACP cancellation cleanup.
  • Requeue only a provable provider refusal; record accepted or transport-unknown delivery as a terminal, retryable-as-new-message outcome.
  • Persist a bounded exact-turn fence in session metadata before touching history, so missing/late history and daemon restarts cannot revive the old steer.
  • Reject duplicate RPC offers and dispatch candidates named by that fence, without changing a newer producer-owned activation.
  • Show an explicit “Delivery unknown” recovery dialog that warns a retry creates a new message and may duplicate provider work.

Visual explanation

sequenceDiagram
    participant UI
    participant CLI
    participant ACP
    participant Meta as Session metadata
    UI->>CLI: steer B
    CLI->>ACP: submit B
    UI->>CLI: Stop turn A
    CLI--xCLI: abort local application wait
    CLI->>ACP: retain/drain raw delivery
    alt ACP proves refusal
        CLI->>CLI: requeue B without rewriting newer pointer C
    else accepted or delivery unknown
        CLI->>Meta: persist exact-id terminal fence for B
        CLI->>CLI: mark visible B failed/unknown if present
    end
    Note over CLI,Meta: restart or late history cannot dispatch fenced B
Loading

Before / after

Before After
Stop could leave the application waiter and steer row unresolved. Stop releases local ownership and reconciles the raw provider delivery during cancellation cleanup.
An ambiguous steer with history not yet synced had only process-local terminal state. A bounded durable exact-id fence blocks duplicate RPC/history dispatch across restarts.
Recovery could leave a perpetual sending state or encourage blind replay. The row is terminal and offers an explicit new-message retry with a duplicate-work warning.

Test plan

  • pnpm --dir apps/cli exec vitest run tests/session-execution-service.test.ts src/session/session-dispatch-logic.test.ts — 126 tests passed, including unresolved delivery with the history row absent.
  • pnpm --dir packages/components exec vitest run tests/undelivered-user-turn.test.ts — 8 tests passed.
  • pnpm --dir packages/shared exec vitest run tests/history-steer-provenance.test.ts — 3 tests passed.
  • Shared, components, and CLI typechecks — passed.
  • Focused Prettier write/check — clean.
  • pnpm run docs check — passed with existing repository warnings only.
  • pnpm run lint:fast — 0 errors (473 existing warnings).
  • pnpm run lint:i18n and code-collab import/platform/public boundary guards — passed.
  • git diff --check upstream/main — passed.

Context handoff

Instructions for reviewing agents

  • Review focus: Trace Stop from steerSessionLocked through cancellation finalization, then verify the exact-id fence in shared activation logic, dispatch watcher, and the UI retry derivation.
  • Decisions to challenge: Check the choice to requeue only AgentSteerNotDeliveredError and to retain a bounded terminal fence rather than adding a second mutable dispatch queue.
  • Plausible failures / evidence gaps: Provider side effects remain inherently unknowable after an ambiguous transport result; tests simulate the protocol and restart state rather than exercising every ACP implementation.

Authoring context

  • User goal / directives: Clean up all leftover state after ACP steer timeout plus Stop, keep the path idempotent, prevent repeated sends, and prevent late results from overwriting newer state.
  • Constraints / non-goals: Do not claim an RPC timeout proves non-delivery, replay ambiguous guidance automatically, or redesign general message ordering.
  • Risk-bearing decisions: A proven refusal follows existing ordinary dispatch recovery, while all other stopped deliveries become exact-id terminal fences before the history write.
  • Destructive or irreversible behavior: No user data is deleted; the original uncertain turn becomes terminal and can only be retried by explicitly creating a new turn.
  • Deliberately not done or tested: No live-provider matrix was run; deterministic protocol mocks cover refusal, accepted/unknown delivery, missing history, newer activation, Stop repetition, restart dispatch suppression, and persisted history normalization.
  • Unknowns / confidence: Confidence is high in the bounded cleanup and duplicate-dispatch fence; the UI intentionally communicates that provider-side execution cannot be reconstructed after transport ambiguity.

Original user prompt

Show original prompt
#666: 遇到 ACP steer 超时后,要清理遗留状态,不要留下副作用,这个逻辑应保持幂等性,避免重复发送和迟到结果覆盖

Keep raw ACP steer delivery in cancellation cleanup, expose ambiguous delivery as a safe new-turn retry, and preserve newer dispatch ownership.

Model: gpt-5.6-sol
Signed-off-by: Dante <duanjl.china@gmail.com>
Declare and round-trip the delivery-unknown marker so HistoryWriter cannot strip the UI recovery contract.

Model: gpt-5.6-sol
Signed-off-by: Dante <duanjl.china@gmail.com>
Model: gpt-5.6-sol

Signed-off-by: Dante <duanjl.china@gmail.com>
Model: gpt-5.6-sol

Signed-off-by: Dante <duanjl.china@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Timed-out steer remains pending_apply after its target turn is stopped

1 participant