summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-contract/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 21:58:29 +0900
committerAdam Malczewski <[email protected]>2026-06-28 21:58:29 +0900
commit01928fe70d22cb7b0a8d20f38b359c8a0048c34b (patch)
treee0ed7cd3ca0b35cf533fb6a7e084b9de7063fc41 /packages/transport-contract/src
parent6dd9ea9b935e5011c16faed6c869c976cf5ff172 (diff)
downloaddispatch-01928fe70d22cb7b0a8d20f38b359c8a0048c34b.tar.gz
dispatch-01928fe70d22cb7b0a8d20f38b359c8a0048c34b.zip
feat(message-queue): cancel a queued steering message by id
Add the ability to cancel/close a single queued message so it never runs: while a turn is GENERATING and a user message sits in the steering queue (waiting for delivery at the next tool-result boundary or carry into a new turn), a client can remove one message by id. The cancelled message is never delivered as steering and never carried into a new turn. Complements the existing chat.queue enqueue (enqueue adds; cancel removes one). Layers (all additive; nothing existing breaks): - message-queue pure core: `cancel(state, conversationId, messageId)` — splices a single message out by id, returns the post-cancel snapshot. Idempotent (no-op if not found). Drops the key when the queue empties (mirrors drain). - message-queue service: `MessageQueueService.cancel()` — wraps the pure op, pushes a surface update ONLY on a real change (queue shrank); a missing-id cancel is a no-op with no surface push (mirrors drain's no-notify-on-empty). - session-orchestrator: `cancelQueuedMessage({ conversationId, messageId })` -> `{ cancelled, queue }`. The single entry transports call; resolves the queue lazily (same as enqueue). Degrades to `{ cancelled: false, queue: [] }` when the message-queue extension isn't loaded. `cancelled` is derived from the queue length delta (true iff a message was removed). API contract (documented for the frontend agent; see frontend-cancel-queued-message-handoff.md): HTTP: DELETE /conversations/:id/queue/:messageId -> 200 QueueCancelResponse { conversationId, cancelled, queue } (cancelled:false is a 200 idempotent no-op, not an error) WS: chat.queue.cancel { type:"chat.queue.cancel", conversationId, messageId } (additive to WsClientMessage). Fire-and-forget like chat.queue: success is confirmed by the message-queue SURFACE updating (the cancelled message leaves payload.messages). A missing-id cancel is a silent no-op (no surface update, no error). Malformed (empty conversationId/ messageId) -> chat.error. No new AgentEvent: a cancelled message never appears in the transcript (it never runs). The existing message-queue surface already reflects the post-cancel snapshot. Race-safe by construction: if the kernel drains the queue (steering) or carries it into a new turn before the cancel runs, the message is already gone -> cancel returns cancelled:false (a no-op). Version bump: @dispatch/transport-contract 0.23.0 -> 0.24.0 (additive: QueueCancelResponse + ChatQueueCancelMessage added to WsClientMessage). @dispatch/wire unchanged (QueuedMessage.id is the cancel target). No CLI command added: the CLI has no queue-listing affordance to discover a messageId, so a CLI cancel would have no input source. The HTTP DELETE is available for any non-WS client that knows the id (e.g. from a prior enqueue response's queue[]). Verification: tsc -b EXIT 0; vitest 2024 passed / 6 skipped (25 new tests); biome EXIT 0 (0 errors).
Diffstat (limited to 'packages/transport-contract/src')
-rw-r--r--packages/transport-contract/src/index.ts42
1 files changed, 41 insertions, 1 deletions
diff --git a/packages/transport-contract/src/index.ts b/packages/transport-contract/src/index.ts
index 797ad22..766abb6 100644
--- a/packages/transport-contract/src/index.ts
+++ b/packages/transport-contract/src/index.ts
@@ -468,6 +468,27 @@ export interface QueueResponse {
readonly queue: readonly QueuedMessage[];
}
+/**
+ * Response body for
+ * `DELETE /conversations/:id/queue/:messageId` — cancel (remove) a single
+ * queued steering message by id so it never runs.
+ *
+ * `cancelled` is `true` when a message with the given id was found in the
+ * conversation's queue and removed (it will never be delivered as steering nor
+ * carried into a new turn). `cancelled` is `false` when the message was not in
+ * the queue (already drained/delivered, never existed, unknown conversation)
+ * OR when the message-queue extension isn't loaded (degraded — feature off).
+ * `queue` is the post-cancel snapshot (empty when no queue extension is
+ * loaded). Idempotent — cancelling a message that is no longer queued returns
+ * `cancelled: false` with HTTP 200 (not an error), so a client may optimistically
+ * fire-and-forget a cancel and reconcile from the surface.
+ */
+export interface QueueCancelResponse {
+ readonly conversationId: string;
+ readonly cancelled: boolean;
+ readonly queue: readonly QueuedMessage[];
+}
+
// ─── Per-conversation LSP status ──────────────────────────────────────────────
/** The connection state of a single language server for a workspace. */
@@ -688,6 +709,24 @@ export interface ChatQueueMessage {
}
/**
+ * Client → server: cancel (remove) a SINGLE queued steering message by id so
+ * it never runs. The WebSocket counterpart of the HTTP
+ * `DELETE /conversations/:id/queue/:messageId` (`QueueCancelResponse`).
+ * Fire-and-forget: success is confirmed by the message-queue SURFACE updating
+ * (the cancelled message leaves the snapshot); a failure (missing/empty
+ * `conversationId` or `messageId`) arrives as a `chat.error`. Idempotent —
+ * cancelling a message that is no longer queued (already drained/delivered) is
+ * a silent no-op (no surface update, no error). `messageId` is the stable
+ * client-visible `QueuedMessage.id` (obtained from the queue surface snapshot
+ * or the enqueue response).
+ */
+export interface ChatQueueCancelMessage {
+ readonly type: "chat.queue.cancel";
+ readonly conversationId: string;
+ readonly messageId: string;
+}
+
+/**
* Every client → server WS message: surface ops (`@dispatch/ui-contract`) + chat
* ops. A server discriminates on `type`.
*/
@@ -696,7 +735,8 @@ export type WsClientMessage =
| ChatSendMessage
| ChatSubscribeMessage
| ChatUnsubscribeMessage
- | ChatQueueMessage;
+ | ChatQueueMessage
+ | ChatQueueCancelMessage;
/**
* Every server → client WS message: surface ops (`@dispatch/ui-contract`) + chat