summaryrefslogtreecommitdiffhomepage
path: root/packages/core
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 20:54:19 +0900
committerAdam Malczewski <[email protected]>2026-05-22 20:54:19 +0900
commitc47346cc6237044ecb60ff22c4011d89744af581 (patch)
tree2359a25e687e1290ba5180fd60eae83b03b53a23 /packages/core
parent288b21cec98421fda57028a0c8c9d835cfbb14b0 (diff)
downloaddispatch-c47346cc6237044ecb60ff22c4011d89744af581.tar.gz
dispatch-c47346cc6237044ecb60ff22c4011d89744af581.zip
feat: message queue/interrupt system, CORS fix, mobile fixes, chat splitting
- Add message queue allowing users to send messages while agent is running - Queue messages are injected into tool results as [USER INTERRUPT] - Retrieve tool interrupted via Promise.race when user message arrives - Queued messages show with 'queued' badge and cancel button - Consumed messages repositioned and chat splits at interrupt point - New assistant message block created after interrupt for clean flow - Add POST /chat/cancel endpoint for cancelling queued messages - Fix CORS to allow any origin (Tailscale/LAN access) - Fix crypto.randomUUID fallback for non-secure contexts (HTTP) - Fix frontend API URL derivation from page hostname - Auto-create DB tab if missing on processMessage (foreign key fix) - Add error logging to processMessage catch block - Fix working directory input sync on agent switch - Fix agent mode button to re-apply agent settings
Diffstat (limited to 'packages/core')
-rw-r--r--packages/core/src/agent/agent.ts76
-rw-r--r--packages/core/src/tools/retrieve.ts28
-rw-r--r--packages/core/src/types/index.ts19
3 files changed, 98 insertions, 25 deletions
diff --git a/packages/core/src/agent/agent.ts b/packages/core/src/agent/agent.ts
index c2c5880..b4c0eec 100644
--- a/packages/core/src/agent/agent.ts
+++ b/packages/core/src/agent/agent.ts
@@ -11,6 +11,7 @@ import type {
AgentEvent,
AgentStatus,
ChatMessage,
+ QueueCallbacks,
ToolCall,
ToolResult,
} from "../types/index.js";
@@ -72,9 +73,11 @@ export class Agent {
messages: ChatMessage[] = [];
private config: AgentConfig;
+ private queueCallbacks?: QueueCallbacks;
- constructor(config: AgentConfig) {
+ constructor(config: AgentConfig, queueCallbacks?: QueueCallbacks) {
this.config = config;
+ this.queueCallbacks = queueCallbacks;
}
private async executeToolWithStreaming(
@@ -185,11 +188,12 @@ export class Agent {
}
try {
- const execPromise = tool.execute(tc.arguments, {
- onOutput: (data: string, stream: "stdout" | "stderr") => {
- shellOutputQueue.push({ data, stream });
- },
- });
+ const execPromise = tool.execute(tc.arguments, {
+ onOutput: (data: string, stream: "stdout" | "stderr") => {
+ shellOutputQueue.push({ data, stream });
+ },
+ queueCallbacks: this.queueCallbacks,
+ });
const rawResult = await execPromise;
return {
@@ -401,15 +405,30 @@ export class Agent {
}
}
- // Drain any remaining shell output emitted before we read the result
- while (shellOutputQueue.length > 0) {
- const item = shellOutputQueue.shift()!;
- yield { type: "shell-output", data: item.data, stream: item.stream };
+ // Drain any remaining shell output emitted before we read the result
+ while (shellOutputQueue.length > 0) {
+ const item = shellOutputQueue.shift()!;
+ yield { type: "shell-output", data: item.data, stream: item.stream };
+ }
+
+ // Check for queued user messages and append them to the tool result
+ let finalToolResult = toolResult;
+ if (this.queueCallbacks) {
+ const queuedMsgs = this.queueCallbacks.dequeueMessages();
+ if (queuedMsgs.length > 0) {
+ const userMessages = queuedMsgs
+ .map((m) => m.message)
+ .join("\n---\n");
+ finalToolResult = {
+ ...toolResult,
+ result: `${toolResult.result}\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`,
+ };
}
+ }
- stepToolResults.push(toolResult);
- allToolResults.push(toolResult);
- yield { type: "tool-result", toolResult };
+ stepToolResults.push(finalToolResult);
+ allToolResults.push(finalToolResult);
+ yield { type: "tool-result", toolResult: finalToolResult };
}
// Add tool results back to step messages so LLM can see them
@@ -431,14 +450,29 @@ export class Agent {
}
}
- const assistantMessage: ChatMessage = {
- role: "assistant",
- content: finalText,
- toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined,
- toolResults: allToolResults.length > 0 ? allToolResults : undefined,
- };
- this.messages.push(assistantMessage);
- yield { type: "done", message: assistantMessage };
+ const assistantMessage: ChatMessage = {
+ role: "assistant",
+ content: finalText,
+ toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined,
+ toolResults: allToolResults.length > 0 ? allToolResults : undefined,
+ };
+ this.messages.push(assistantMessage);
+
+ // Drain any remaining queued messages that arrived after the last tool call
+ if (this.queueCallbacks) {
+ const remaining = this.queueCallbacks.dequeueMessages();
+ if (remaining.length > 0) {
+ // These messages arrived too late to be injected into a tool result.
+ // Append them as a user message to the conversation so they're not lost.
+ const userMessages = remaining.map(m => m.message).join("\n---\n");
+ this.messages.push({
+ role: "user",
+ content: userMessages,
+ });
+ }
+ }
+
+ yield { type: "done", message: assistantMessage };
} catch (err) {
const errorMsg = formatError(err, this.config);
yield { type: "error", error: errorMsg };
diff --git a/packages/core/src/tools/retrieve.ts b/packages/core/src/tools/retrieve.ts
index 93f4c89..8e14a96 100644
--- a/packages/core/src/tools/retrieve.ts
+++ b/packages/core/src/tools/retrieve.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import type { ToolDefinition } from "../types/index.js";
+import type { ToolDefinition, ToolExecuteContext } from "../types/index.js";
export interface RetrieveCallbacks {
getResult(
@@ -24,11 +24,33 @@ export function createRetrieveTool(callbacks: RetrieveCallbacks): ToolDefinition
parameters: z.object({
agent_id: z.string().describe("The agent_id returned by a previous summon call."),
}),
- execute: async (args: Record<string, unknown>): Promise<string> => {
+ execute: async (args: Record<string, unknown>, context?: ToolExecuteContext): Promise<string> => {
const agentId = args.agent_id as string;
+ const queueCallbacks = context?.queueCallbacks;
try {
- const outcome = await callbacks.getResult(agentId);
+ let outcome: { status: "done"; result: string } | { status: "error"; error: string };
+
+ if (queueCallbacks) {
+ const childPromise = callbacks.getResult(agentId);
+ const { promise: queuePromise, cancel: cancelQueueWait } = queueCallbacks.waitForQueuedMessage();
+ const queueSignal = queuePromise.then(() => "QUEUE_INTERRUPT" as const);
+
+ const raceResult = await Promise.race([childPromise, queueSignal]);
+
+ if (raceResult === "QUEUE_INTERRUPT") {
+ const queuedMsgs = queueCallbacks.dequeueMessages();
+ const userMessages = queuedMsgs.map((m) => m.message).join("\n---\n");
+ return `The subagent (agent_id: ${agentId}) has not completed its task yet. You will need to call retrieve with this agent_id again later to get the result.\n\n[USER INTERRUPT]\nThe user has sent you message(s) while you were working. You MUST address these before continuing with your current task:\n\n${userMessages}`;
+ }
+
+ // Child finished first — clean up the queue listener
+ cancelQueueWait();
+ outcome = raceResult;
+ } else {
+ outcome = await callbacks.getResult(agentId);
+ }
+
if (outcome.status === "done") {
return ["<agent_result>", outcome.result, "</agent_result>"].join("\n");
}
diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts
index 8f80b08..be37674 100644
--- a/packages/core/src/types/index.ts
+++ b/packages/core/src/types/index.ts
@@ -47,12 +47,16 @@ export type AgentEvent =
keyId: string | null;
modelId: string | null;
parentTabId: string | null;
- };
+ }
+ | { type: "message-queued"; tabId: string; messageId: string; message: string }
+ | { type: "message-consumed"; tabId: string; messageIds: string[] }
+ | { type: "message-cancelled"; tabId: string; messageId: string };
// ─── Tool Types ──────────────────────────────────────────────────
export interface ToolExecuteContext {
onOutput?: (data: string, stream: "stdout" | "stderr") => void;
+ queueCallbacks?: QueueCallbacks;
}
export interface ToolDefinition {
@@ -147,6 +151,19 @@ export interface ConfigError {
message: string;
}
+// ─── Message Queue Types ─────────────────────────────────────────
+
+export interface QueuedMessage {
+ id: string;
+ message: string;
+ timestamp: number;
+}
+
+export interface QueueCallbacks {
+ dequeueMessages: () => QueuedMessage[];
+ waitForQueuedMessage: () => { promise: Promise<void>; cancel: () => void };
+}
+
// ─── Agent Definition Types ──────────────────────────────────────
export interface AgentModelEntry {