fix(coding-agent/session): split queued-message editor restore between dequeue and interrupt

- Generalized `isUserQueuedMessage` to a user-attribution predicate (`role === "user"` or custom `attribution === "user"` and not display-suppressed) so visible agent-authored steers (advisor cards, IRC/extension asides) and hidden goal/plan/budget steers are all excluded from editor restore, not just advisor cards.
- Gave `AgentSession.clearQueue` a `{ forInterrupt }` option: plain Alt+Up dequeue restores user messages and preserves every other queued message for the continuing stream, while Esc+abort keeps only advisor cards (for `abort()`'s `#extractQueuedAdvisorCards` preservation) and drops other internal steers so the post-abort `#drainStrandedQueuedMessages` can't auto-resume the interrupted run.
- Threaded `forInterrupt: options?.abort` from `InputController.restoreQueuedMessagesToEditor` and kept `queuedMessageCount` on actual displayable-queue semantics so `hasPendingMessages()`/RPC and the empty-submit abort gate stay accurate.
- Updated skill-queue tests to cover both policies (hidden and visible agent-authored steers preserved on dequeue, dropped on interrupt) and refreshed the changelog entry.
This commit is contained in:
can1357
2026-06-16 16:09:28 +02:00
parent 6bbc573c6f
commit 1e3909a151
5 changed files with 44 additions and 28 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
### Fixed
- Fixed interactive submissions made while the TUI had no active input waiter: they now start a real prompt directly, with steer fallback if a background turn races in, instead of queueing behind a non-resumable idle transcript and appearing to do nothing.
- Fixed pressing Esc (or Alt+Up dequeue) while agent-authored messages were queued — advisor concern/blocker notes, hidden goal/plan/budget steers, IRC/extension asides — dumping their text into the user's editor. Editor restoration (`clearQueue()`), pending chips (`getQueuedMessages()`), and `popLastQueuedMessage()` now surface only genuinely user-authored queued messages (plain user turns and `attribution: "user"` custom messages like `/skill`). Advisor cards are left queued so the user-interrupt abort path still re-records them as visible advice; other agent-authored steers are dropped on clear so a user interrupt can't be silently undone by an auto-resume on leftover internal context. `queuedMessageCount` still reflects all actual queued work (advisor cards included) so `hasPendingMessages()`/RPC and the empty-submit abort gate stay accurate.
- Fixed pressing Esc (or Alt+Up dequeue) while agent-authored messages were queued — advisor concern/blocker notes, hidden goal/plan/budget steers, IRC/extension asides — dumping their text into the user's editor. Editor restoration (`clearQueue()`), pending chips (`getQueuedMessages()`), and `popLastQueuedMessage()` now surface only genuinely user-authored queued messages (plain user turns and `attribution: "user"` custom messages like `/skill`). Plain Alt+Up dequeue leaves all other queued messages in place for the continuing stream; only the Esc interrupt path keeps just advisor cards (so abort's preservation still re-records them as visible advice) and drops other internal steers, so a user interrupt can't be silently undone by an auto-resume on leftover internal context. `queuedMessageCount` still reflects all actual queued work (advisor cards included) so `hasPendingMessages()`/RPC and the empty-submit abort gate stay accurate.
- Fixed `omp --continue`/`-c` sometimes resuming into a subagent transcript instead of the interactive session. Subagent (and HTML-export) `SessionManager.open()` calls run in the parent's terminal and were clobbering the per-TTY `--continue` breadcrumb with their own artifact-dir session file; these headless opens now suppress the breadcrumb. `continueRecent()` also recovers already-poisoned breadcrumbs by resolving any session file inside a parent's artifacts dir (`<parent>/<agentId>.jsonl`) back up to the top-level session.
## [16.0.2] - 2026-06-16
@@ -998,7 +998,9 @@ export class InputController {
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
this.ctx.locallySubmittedUserSignatures.clear();
const { steering, followUp } = this.ctx.session.clearQueue();
// On Esc (abort) drop non-user internal steers so the post-abort drain can't
// auto-resume; plain Alt+Up dequeue preserves them for the continuing stream.
const { steering, followUp } = this.ctx.session.clearQueue({ forInterrupt: options?.abort });
// Messages typed while compacting live in `compactionQueuedMessages`, not the
// agent queue `clearQueue()` drains — but the pending bar shows the same
// "Alt+Up to edit" hint for them (ui-helpers `updatePendingMessagesDisplay`).
@@ -5885,21 +5885,24 @@ export class AgentSession {
});
}
/** Clear queued messages and return them (text plus any attached images). */
clearQueue(): { steering: RestoredQueuedMessage[]; followUp: RestoredQueuedMessage[] } {
/** Clear queued messages and return the user-restorable ones (text plus any attached images).
* Only user-authored messages (plain user turns, `attribution:"user"` custom like `/skill`) are
* returned for editor restore. Other queued messages stay in the agent-core queues so a continuing
* stream still delivers them — EXCEPT on `forInterrupt` (Esc+abort), where only advisor cards are
* kept (abort()'s #extractQueuedAdvisorCards preserves them as visible advice) and every other
* non-user steer (hidden goal/plan/budget, IRC/extension asides) is dropped, so abort()'s
* #drainStrandedQueuedMessages can't auto-resume the run the user just interrupted (the drain only
* fires while agent.hasQueuedMessages()). Plain Alt+Up dequeue preserves those non-user steers. */
clearQueue(options?: { forInterrupt?: boolean }): {
steering: RestoredQueuedMessage[];
followUp: RestoredQueuedMessage[];
} {
const steeringAll = this.agent.peekSteeringQueue();
const followUpAll = this.agent.peekFollowUpQueue();
const steering = steeringAll.filter(isUserQueuedMessage).map(toRestoredQueuedMessage);
const followUp = followUpAll.filter(isUserQueuedMessage).map(toRestoredQueuedMessage);
// User-authored messages go back to the editor. Advisor concern/blocker cards
// are kept queued: a resumed stream still delivers them, and on Esc abort()'s
// #extractQueuedAdvisorCards extracts + preserves them as visible advice
// (with auto-resume suppressed). Every other non-user queued message — hidden
// goal/plan/budget steers, IRC/extension asides — is dropped, matching the
// original full clear: keeping them would both leak into the editor and let
// abort()'s #drainStrandedQueuedMessages silently auto-resume the run the user
// just interrupted (the drain only fires while agent.hasQueuedMessages()).
this.agent.replaceQueues(steeringAll.filter(isAdvisorCard), followUpAll.filter(isAdvisorCard));
const keep: (m: AgentMessage) => boolean = options?.forInterrupt ? isAdvisorCard : m => !isUserQueuedMessage(m);
this.agent.replaceQueues(steeringAll.filter(keep), followUpAll.filter(keep));
return { steering, followUp };
}
@@ -286,34 +286,42 @@ describe("AgentSession derived queued custom display", () => {
expect(session.getQueuedMessages().steering).toEqual([]);
expect(session.queuedMessageCount).toBe(0);
// A hidden agent-authored steer must not leak into the editor, and clearQueue
// must drop it so abort()'s stranded-message drain can't auto-resume the run
// the user just interrupted (the drain gate is agent.hasQueuedMessages()).
// Plain Alt+Up dequeue restores nothing AND preserves the hidden steer for the
// continuing stream — it isn't the user's draft.
expect(session.clearQueue().steering).toEqual([]);
expect(session.agent.hasQueuedMessages()).toBe(true);
// Esc+abort drops it so abort()'s stranded-message drain can't auto-resume the
// run the user just interrupted (the drain gate is agent.hasQueuedMessages()).
expect(session.clearQueue({ forInterrupt: true }).steering).toEqual([]);
expect(session.agent.hasQueuedMessages()).toBe(false);
});
it("never restores a visible agent-authored custom steer, and drops it on clearQueue", async () => {
it("never restores a visible agent-authored custom steer; preserves on dequeue, drops on interrupt", async () => {
fixture = await createRealSession();
const { session } = fixture;
// An IRC aside / extension notice: visible, but agent-authored — editing it
// An IRC aside / extension/hook notice: visible, but agent-authored — editing it
// makes no sense, so it must not ride the Esc/Alt+Up editor-restore path.
session.agent.steer({
role: "custom",
customType: "irc",
content: "peer pinged you",
display: true,
attribution: "agent",
details: {},
timestamp: Date.now(),
});
const steer = () =>
session.agent.steer({
role: "custom",
customType: "irc",
content: "peer pinged you",
display: true,
attribution: "agent",
details: {},
timestamp: Date.now(),
});
steer();
expect(session.getQueuedMessages().steering).toEqual([]);
// popLast leaves the agent steer untouched (not user-restorable)...
expect(session.popLastQueuedMessage()).toBeUndefined();
expect(session.agent.peekSteeringQueue()).toHaveLength(1);
// ...and clearQueue restores nothing while dropping it (no auto-resume leftover).
// ...plain dequeue restores nothing but PRESERVES the extension steer (not lost)...
expect(session.clearQueue().steering).toEqual([]);
expect(session.agent.peekSteeringQueue()).toHaveLength(1);
// ...and only Esc+abort drops it (no auto-resume leftover).
expect(session.clearQueue({ forInterrupt: true }).steering).toEqual([]);
expect(session.agent.hasQueuedMessages()).toBe(false);
});
+3
View File
@@ -63,6 +63,9 @@
"isPartOf": { "@type": "WebSite", "name": "omp", "url": "https://omp.sh/" }
}
</script>
<!-- Analytics. data-exclude-hash keeps the E2E room key (URL fragment) out of analytics. -->
<script defer src="https://um.can.ac/script.js" data-website-id="28ab5a7d-3ca4-4c85-9da5-666fa731cb92" data-exclude-hash="true"></script>
</head>
<body>
<div id="root"></div>