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:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user