chapterhouse 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api/auth.js +11 -1
- package/dist/api/auth.test.js +29 -0
- package/dist/api/errors.js +23 -0
- package/dist/api/route-coverage.test.js +61 -21
- package/dist/api/routes/agents.js +472 -0
- package/dist/api/routes/memory.js +299 -0
- package/dist/api/routes/projects.js +170 -0
- package/dist/api/routes/sessions.js +347 -0
- package/dist/api/routes/system.js +82 -0
- package/dist/api/routes/wiki.js +455 -0
- package/dist/api/routes/wiki.test.js +49 -0
- package/dist/api/send-json.js +16 -0
- package/dist/api/send-json.test.js +18 -0
- package/dist/api/server-runtime.js +45 -3
- package/dist/api/server.js +34 -1764
- package/dist/api/server.test.js +239 -8
- package/dist/api/sse-hub.js +37 -0
- package/dist/cli.js +1 -1
- package/dist/config.js +151 -58
- package/dist/config.test.js +29 -0
- package/dist/copilot/okr-mapper.js +2 -11
- package/dist/copilot/orchestrator.js +358 -352
- package/dist/copilot/orchestrator.test.js +139 -4
- package/dist/copilot/prompt-date.js +2 -1
- package/dist/copilot/session-manager.js +25 -23
- package/dist/copilot/session-manager.test.js +35 -1
- package/dist/copilot/standup.js +2 -2
- package/dist/copilot/task-event-log.js +7 -1
- package/dist/copilot/task-event-log.test.js +13 -0
- package/dist/copilot/tools/agent.js +608 -0
- package/dist/copilot/tools/index.js +19 -0
- package/dist/copilot/tools/memory.js +678 -0
- package/dist/copilot/tools/models.js +2 -0
- package/dist/copilot/tools/okr.js +171 -0
- package/dist/copilot/tools/wiki.js +333 -0
- package/dist/copilot/tools-deps.js +4 -0
- package/dist/copilot/tools.agent.test.js +10 -8
- package/dist/copilot/tools.inventory.test.js +76 -0
- package/dist/copilot/tools.js +1 -1780
- package/dist/copilot/tools.okr.test.js +31 -0
- package/dist/copilot/tools.wiki.test.js +6 -3
- package/dist/copilot/turn-event-log.js +31 -4
- package/dist/copilot/turn-event-log.test.js +24 -2
- package/dist/copilot/workiq-installer.test.js +2 -2
- package/dist/daemon-install.js +3 -2
- package/dist/daemon.js +9 -17
- package/dist/integrations/ado-client.js +90 -9
- package/dist/integrations/ado-client.test.js +56 -0
- package/dist/integrations/team-push.js +1 -0
- package/dist/integrations/team-push.test.js +6 -0
- package/dist/integrations/teams-notify.js +1 -0
- package/dist/integrations/teams-notify.test.js +5 -0
- package/dist/memory/active-scope.test.js +0 -1
- package/dist/memory/checkpoint.js +89 -72
- package/dist/memory/checkpoint.test.js +23 -3
- package/dist/memory/eot.js +87 -85
- package/dist/memory/eot.test.js +71 -3
- package/dist/memory/hooks.js +2 -4
- package/dist/memory/housekeeping-scheduler.js +1 -1
- package/dist/memory/housekeeping-scheduler.test.js +1 -2
- package/dist/memory/housekeeping.js +100 -3
- package/dist/memory/housekeeping.test.js +33 -2
- package/dist/memory/reflect.test.js +2 -0
- package/dist/memory/scope-lock.js +26 -0
- package/dist/memory/scope-lock.test.js +118 -0
- package/dist/memory/scopes.test.js +0 -1
- package/dist/mode-context.js +58 -5
- package/dist/mode-context.test.js +68 -0
- package/dist/paths.js +1 -0
- package/dist/setup.js +3 -2
- package/dist/shared/api-schemas.js +48 -5
- package/dist/store/connection.js +96 -0
- package/dist/store/db.js +5 -1498
- package/dist/store/db.test.js +182 -1
- package/dist/store/migrations.js +460 -0
- package/dist/store/repositories/memory.js +281 -0
- package/dist/store/repositories/okr.js +3 -0
- package/dist/store/repositories/projects.js +5 -0
- package/dist/store/repositories/sessions.js +284 -0
- package/dist/store/repositories/wiki.js +60 -0
- package/dist/store/schema.js +501 -0
- package/dist/util/logger.js +3 -2
- package/dist/wiki/consolidation.js +50 -9
- package/dist/wiki/consolidation.test.js +45 -0
- package/dist/wiki/frontmatter.js +43 -13
- package/dist/wiki/frontmatter.test.js +24 -0
- package/dist/wiki/fs.js +16 -4
- package/dist/wiki/fs.test.js +84 -0
- package/dist/wiki/index-manager.js +30 -2
- package/dist/wiki/index-manager.test.js +43 -12
- package/dist/wiki/ingest.js +1 -1
- package/dist/wiki/lock.js +11 -1
- package/dist/wiki/log-manager.js +2 -7
- package/dist/wiki/migrate.js +44 -17
- package/dist/wiki/project-registry.js +10 -5
- package/dist/wiki/project-registry.test.js +14 -0
- package/dist/wiki/scheduler.js +1 -1
- package/dist/wiki/seed-team-wiki.js +2 -1
- package/dist/wiki/team-sync.js +31 -6
- package/dist/wiki/team-sync.test.js +81 -0
- package/package.json +1 -1
- package/web/dist/assets/WikiEdit-EBVoY1Pk.js +30 -0
- package/web/dist/assets/WikiEdit-EBVoY1Pk.js.map +1 -0
- package/web/dist/assets/WikiGraph-BUbbABq-.js +2 -0
- package/web/dist/assets/WikiGraph-BUbbABq-.js.map +1 -0
- package/web/dist/assets/icon-acolyte-cream.svg +10 -0
- package/web/dist/assets/icon-acolyte-dark.svg +10 -0
- package/web/dist/assets/icon-acolyte-gold.svg +10 -0
- package/web/dist/assets/icon-acolyte-ibad.svg +10 -0
- package/web/dist/assets/icon-acolyte-lit.svg +10 -0
- package/web/dist/assets/icon-acolyte-mono.svg +10 -0
- package/web/dist/assets/icon-acolyte.png +0 -0
- package/web/dist/assets/icon-acolyte.svg +10 -0
- package/web/dist/assets/index-BGLL9pgM.css +10 -0
- package/web/dist/assets/index-KFX8UmOb.js +250 -0
- package/web/dist/assets/index-KFX8UmOb.js.map +1 -0
- package/web/dist/index.html +6 -4
- package/web/dist/assets/index-5kz9aRU9.css +0 -10
- package/web/dist/assets/index-iQrv3lQN.js +0 -286
- package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
|
@@ -11,7 +11,7 @@ import { config, DEFAULT_MODEL } from "../config.js";
|
|
|
11
11
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
12
12
|
import { getSkillDirectories } from "./skills.js";
|
|
13
13
|
import { resetClient } from "./client.js";
|
|
14
|
-
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey, getDb, appendTaskEvent } from "../store/db.js";
|
|
14
|
+
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, deleteCopilotSession, getTaskSessionKey, getDb, appendTaskEvent } from "../store/db.js";
|
|
15
15
|
import { maybeWriteEpisode } from "./episode-writer.js";
|
|
16
16
|
import { SESSIONS_DIR } from "../paths.js";
|
|
17
17
|
import { resolveModel } from "./router.js";
|
|
@@ -27,14 +27,17 @@ import { detectProjectRuleWarnings } from "./project-rule-warnings.js";
|
|
|
27
27
|
import { loadRegistry } from "../wiki/project-registry.js";
|
|
28
28
|
import { loadProjectRules } from "../wiki/project-rules.js";
|
|
29
29
|
import { resolveProject } from "./project-resolution.js";
|
|
30
|
+
import { stopClassifier } from "./classifier.js";
|
|
30
31
|
const log = childLogger("orchestrator");
|
|
31
32
|
const MAX_RETRIES = 3;
|
|
32
|
-
const
|
|
33
|
+
const RECONNECT_BASE_DELAY_MS = 1_000;
|
|
34
|
+
const RECONNECT_MAX_DELAY_MS = 10_000;
|
|
33
35
|
const HEALTH_CHECK_INTERVAL_MS = 30_000;
|
|
34
36
|
const AGENT_REPLY_CHUNK_SIZE = 500;
|
|
35
37
|
const AGENT_REPLY_CHUNK_THRESHOLD = 8 * 1024;
|
|
36
38
|
const ORCHESTRATOR_SESSION_KEY = "orchestrator_session_id";
|
|
37
39
|
const LAST_AUTHENTICATED_USER_KEY = "last_authenticated_user";
|
|
40
|
+
let taskEventLogCleanup;
|
|
38
41
|
function getWikiSummary() {
|
|
39
42
|
try {
|
|
40
43
|
const db = getDb();
|
|
@@ -75,14 +78,6 @@ const turnContextStorage = new AsyncLocalStorage();
|
|
|
75
78
|
let copilotClient;
|
|
76
79
|
let healthCheckTimer;
|
|
77
80
|
let currentUserContext;
|
|
78
|
-
/**
|
|
79
|
-
* Last-seen auth context — persists after a turn completes so that callers
|
|
80
|
-
* which inspect it outside of an active turn (e.g. /api/cancel) still see the
|
|
81
|
-
* most recent values. Tools that run DURING a turn should use the per-turn
|
|
82
|
-
* AsyncLocalStorage context for safety in concurrent-session scenarios.
|
|
83
|
-
*/
|
|
84
|
-
let currentAuthenticatedUser;
|
|
85
|
-
let currentAuthorizationHeader;
|
|
86
81
|
let lastRouteResult;
|
|
87
82
|
let memoryCoordinator;
|
|
88
83
|
export function getLastRouteResult() {
|
|
@@ -155,7 +150,7 @@ export function getCurrentActiveProjectRules() {
|
|
|
155
150
|
return turnContextStorage.getStore()?.activeProjectRules ?? null;
|
|
156
151
|
}
|
|
157
152
|
export function getCurrentAuthenticatedUser() {
|
|
158
|
-
return turnContextStorage.getStore()?.authUser
|
|
153
|
+
return turnContextStorage.getStore()?.authUser;
|
|
159
154
|
}
|
|
160
155
|
export function getLastAuthenticatedUser() {
|
|
161
156
|
const raw = getState(LAST_AUTHENTICATED_USER_KEY);
|
|
@@ -172,7 +167,7 @@ export function getLastAuthenticatedUser() {
|
|
|
172
167
|
}
|
|
173
168
|
}
|
|
174
169
|
export function getCurrentAuthorizationHeader() {
|
|
175
|
-
return turnContextStorage.getStore()?.authHeader
|
|
170
|
+
return turnContextStorage.getStore()?.authHeader;
|
|
176
171
|
}
|
|
177
172
|
// ---------------------------------------------------------------------------
|
|
178
173
|
// Internal helpers
|
|
@@ -238,14 +233,9 @@ function updateUserContext(source) {
|
|
|
238
233
|
export function invalidateOrchestratorSession(sessionKey) {
|
|
239
234
|
registry?.get(sessionKey)?.invalidateSession();
|
|
240
235
|
}
|
|
241
|
-
function
|
|
242
|
-
if (source.type !== "web" && source.type !== "sse-web")
|
|
243
|
-
currentAuthenticatedUser = undefined;
|
|
244
|
-
currentAuthorizationHeader = undefined;
|
|
236
|
+
function recordLastAuthenticatedUser(source) {
|
|
237
|
+
if (source.type !== "web" && source.type !== "sse-web")
|
|
245
238
|
return;
|
|
246
|
-
}
|
|
247
|
-
currentAuthenticatedUser = source.user;
|
|
248
|
-
currentAuthorizationHeader = source.authorizationHeader?.trim() || undefined;
|
|
249
239
|
if (source.user) {
|
|
250
240
|
setState(LAST_AUTHENTICATED_USER_KEY, JSON.stringify(source.user));
|
|
251
241
|
}
|
|
@@ -290,7 +280,7 @@ export function feedAgentResult(taskId, agentSlug, result) {
|
|
|
290
280
|
catch (err) {
|
|
291
281
|
log.warn({ err: err instanceof Error ? err.message : err, taskId, agentSlug }, "Failed to persist agent completion");
|
|
292
282
|
}
|
|
293
|
-
|
|
283
|
+
const ackPromise = (async () => {
|
|
294
284
|
await new Promise((resolve) => setImmediate(resolve));
|
|
295
285
|
const prompt = `[Agent task completed] @${agentSlug} finished task ${taskId}. The user has already seen this reply in the agent's own bubble. Acknowledge briefly without restating content.`;
|
|
296
286
|
sendToOrchestrator(prompt, { type: "background", sessionKey }, (text, done) => {
|
|
@@ -299,6 +289,9 @@ export function feedAgentResult(taskId, agentSlug, result) {
|
|
|
299
289
|
}
|
|
300
290
|
}, undefined, undefined, undefined, undefined, undefined, { suppressPromptLog: true });
|
|
301
291
|
})();
|
|
292
|
+
void ackPromise.catch((err) => {
|
|
293
|
+
log.error({ err, taskId, agentSlug, sessionKey }, "unhandled rejection in feedAgentResult");
|
|
294
|
+
});
|
|
302
295
|
}
|
|
303
296
|
function sleep(ms) {
|
|
304
297
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -407,6 +400,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
407
400
|
}
|
|
408
401
|
catch (err) {
|
|
409
402
|
log.warn({ sessionKey, err: err instanceof Error ? err.message : err }, "Could not resume session, creating new");
|
|
403
|
+
deleteCopilotSession(sessionKey);
|
|
410
404
|
if (sessionKey === "default")
|
|
411
405
|
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
412
406
|
}
|
|
@@ -443,7 +437,8 @@ export async function initOrchestrator(client) {
|
|
|
443
437
|
resolveScopeForSession: getMemoryScopeForSession,
|
|
444
438
|
});
|
|
445
439
|
// Initialize per-task ring buffer — subscribes to agentEventBus for session:tool_call events.
|
|
446
|
-
|
|
440
|
+
taskEventLogCleanup?.();
|
|
441
|
+
taskEventLogCleanup = initTaskEventLog();
|
|
447
442
|
// (Re-)create the registry — supports multiple initOrchestrator calls in tests
|
|
448
443
|
if (registry) {
|
|
449
444
|
await registry.shutdown();
|
|
@@ -485,16 +480,7 @@ export async function initOrchestrator(client) {
|
|
|
485
480
|
* Override with CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS env var (parsed as integer ms).
|
|
486
481
|
* Applies per-session-turn; concurrent sessions each have their own independent timeout.
|
|
487
482
|
* Part of the 3-layer timing contract — see systemd unit TimeoutStopSec comment. */
|
|
488
|
-
const
|
|
489
|
-
export const ORCHESTRATOR_TIMEOUT_MS = (() => {
|
|
490
|
-
const env = process.env.CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS;
|
|
491
|
-
if (env) {
|
|
492
|
-
const parsed = parseInt(env, 10);
|
|
493
|
-
if (!isNaN(parsed) && parsed > 0)
|
|
494
|
-
return parsed;
|
|
495
|
-
}
|
|
496
|
-
return DEFAULT_ORCHESTRATOR_TIMEOUT_MS;
|
|
497
|
-
})();
|
|
483
|
+
export const ORCHESTRATOR_TIMEOUT_MS = config.orchestratorTimeoutMs;
|
|
498
484
|
/**
|
|
499
485
|
* Execute a single queued item on its session.
|
|
500
486
|
* Wraps the entire turn in AsyncLocalStorage so all tool handlers (e.g. delegate_to_agent,
|
|
@@ -512,9 +498,6 @@ async function executeOnSession(manager, item) {
|
|
|
512
498
|
const sessionPrompt = activeProjectRules
|
|
513
499
|
? `${warningBlock}${renderActiveProjectRulesBlock(activeProjectRules.project.slug, activeProjectRules.project.path, activeProjectRules.rules.path, activeProjectRules.rules.hard, activeProjectRules.rules.soft)}\n\n${item.prompt}`
|
|
514
500
|
: item.prompt;
|
|
515
|
-
// Update last-seen globals (backwards compat — for callers that inspect after a turn ends)
|
|
516
|
-
currentAuthenticatedUser = item.authUser;
|
|
517
|
-
currentAuthorizationHeader = item.authHeader;
|
|
518
501
|
const runTurn = () => turnContextStorage.run({
|
|
519
502
|
sessionKey,
|
|
520
503
|
sourceChannel: item.sourceChannel,
|
|
@@ -532,327 +515,332 @@ async function executeOnSession(manager, item) {
|
|
|
532
515
|
// actual spawn parameters (name, description) passed to the task() tool call.
|
|
533
516
|
const spawnArgsMap = new Map();
|
|
534
517
|
const toolStartDetails = new Map();
|
|
535
|
-
// Unconditional capture — must fire even when onActivity is absent so the DB handler can resolve names.
|
|
536
|
-
const unsubSpawnCapture = session.on("tool.execution_start", (event) => {
|
|
537
|
-
const data = event.data;
|
|
538
|
-
if (data.toolCallId) {
|
|
539
|
-
toolStartDetails.set(data.toolCallId, {
|
|
540
|
-
toolName: String(data.toolName ?? "unknown"),
|
|
541
|
-
mcpServerName: typeof data.mcpServerName === "string" ? data.mcpServerName : undefined,
|
|
542
|
-
arguments: data.arguments,
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
if (data.toolName === "task" && data.toolCallId) {
|
|
546
|
-
const args = (data.arguments ?? {});
|
|
547
|
-
spawnArgsMap.set(data.toolCallId, {
|
|
548
|
-
name: typeof args.name === "string" ? args.name : undefined,
|
|
549
|
-
description: typeof args.description === "string" ? args.description : undefined,
|
|
550
|
-
prompt: typeof args.prompt === "string"
|
|
551
|
-
? args.prompt
|
|
552
|
-
: typeof args.task === "string"
|
|
553
|
-
? args.task
|
|
554
|
-
: undefined,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
const unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
559
|
-
toolCallExecuted = true;
|
|
560
|
-
toolCallCount++;
|
|
561
|
-
const data = event.data;
|
|
562
|
-
const result = data.result;
|
|
563
|
-
const resultPreview = typeof result?.content === "string" ? result.content.slice(0, 400) : undefined;
|
|
564
|
-
const detailedContent = typeof result?.detailedContent === "string"
|
|
565
|
-
? result.detailedContent
|
|
566
|
-
: typeof result?.content === "string"
|
|
567
|
-
? result.content
|
|
568
|
-
: undefined;
|
|
569
|
-
const toolCallId = String(data.toolCallId ?? "");
|
|
570
|
-
const startDetails = toolStartDetails.get(toolCallId);
|
|
571
|
-
const completionToolName = data.toolName;
|
|
572
|
-
if (item.onActivity) {
|
|
573
|
-
item.onActivity({
|
|
574
|
-
kind: "tool_complete",
|
|
575
|
-
toolCallId: data.toolCallId,
|
|
576
|
-
success: data.success,
|
|
577
|
-
resultPreview,
|
|
578
|
-
detailedContent,
|
|
579
|
-
}, item.turnId);
|
|
580
|
-
}
|
|
581
|
-
// Emit turn:delta with tool-call part (coexistence — #130)
|
|
582
|
-
const toolPart = {
|
|
583
|
-
type: "tool-call",
|
|
584
|
-
toolCallId,
|
|
585
|
-
toolName: typeof completionToolName === "string" && completionToolName.length > 0
|
|
586
|
-
? completionToolName
|
|
587
|
-
: (startDetails?.toolName ?? "unknown"),
|
|
588
|
-
mcpServerName: startDetails?.mcpServerName,
|
|
589
|
-
arguments: startDetails?.arguments,
|
|
590
|
-
status: data.success !== false ? "done" : "failed",
|
|
591
|
-
resultPreview,
|
|
592
|
-
detailedContent,
|
|
593
|
-
};
|
|
594
|
-
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part: toolPart });
|
|
595
|
-
if (toolCallId) {
|
|
596
|
-
toolStartDetails.delete(toolCallId);
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
const unsubToolStart = item.onActivity
|
|
600
|
-
? session.on("tool.execution_start", (event) => {
|
|
601
|
-
const data = event.data;
|
|
602
|
-
item.onActivity({
|
|
603
|
-
kind: "tool_start",
|
|
604
|
-
toolCallId: data.toolCallId,
|
|
605
|
-
toolName: data.toolName,
|
|
606
|
-
mcpServerName: data.mcpServerName,
|
|
607
|
-
arguments: data.arguments,
|
|
608
|
-
}, item.turnId);
|
|
609
|
-
})
|
|
610
|
-
: () => { };
|
|
611
|
-
const unsubReasoning = item.onActivity
|
|
612
|
-
? session.on("assistant.reasoning_delta", (event) => {
|
|
613
|
-
item.onActivity({
|
|
614
|
-
kind: "thinking_delta",
|
|
615
|
-
reasoningId: event.data.reasoningId,
|
|
616
|
-
deltaContent: event.data.deltaContent,
|
|
617
|
-
}, item.turnId);
|
|
618
|
-
})
|
|
619
|
-
: () => { };
|
|
620
|
-
const unsubSubStart = item.onActivity
|
|
621
|
-
? session.on("subagent.started", (event) => {
|
|
622
|
-
const data = event.data;
|
|
623
|
-
const spawnArgs = spawnArgsMap.get(data.toolCallId);
|
|
624
|
-
const agentSlug = (typeof spawnArgs?.name === "string" ? spawnArgs.name : (data.agentName || "unknown"))
|
|
625
|
-
.toLowerCase()
|
|
626
|
-
.replace(/\s+/g, "-");
|
|
627
|
-
const resolvedDescription = (typeof spawnArgs?.description === "string"
|
|
628
|
-
? spawnArgs.description
|
|
629
|
-
: data.agentDescription || data.agentDisplayName || `Agent dispatch: ${agentSlug}`).slice(0, 500);
|
|
630
|
-
item.onActivity({
|
|
631
|
-
kind: "subagent_started",
|
|
632
|
-
toolCallId: data.toolCallId,
|
|
633
|
-
agentName: data.agentName,
|
|
634
|
-
agentDisplayName: data.agentDisplayName,
|
|
635
|
-
agentDescription: resolvedDescription,
|
|
636
|
-
agentSlug,
|
|
637
|
-
}, item.turnId);
|
|
638
|
-
})
|
|
639
|
-
: () => { };
|
|
640
|
-
const unsubSubDone = item.onActivity
|
|
641
|
-
? session.on("subagent.completed", (event) => {
|
|
642
|
-
const data = event.data;
|
|
643
|
-
item.onActivity({
|
|
644
|
-
kind: "subagent_completed",
|
|
645
|
-
toolCallId: data.toolCallId,
|
|
646
|
-
agentName: data.agentName,
|
|
647
|
-
agentDisplayName: data.agentDisplayName,
|
|
648
|
-
durationMs: data.durationMs,
|
|
649
|
-
}, item.turnId);
|
|
650
|
-
})
|
|
651
|
-
: () => { };
|
|
652
|
-
const unsubSubFail = item.onActivity
|
|
653
|
-
? session.on("subagent.failed", (event) => {
|
|
654
|
-
const data = event.data;
|
|
655
|
-
item.onActivity({
|
|
656
|
-
kind: "subagent_failed",
|
|
657
|
-
toolCallId: data.toolCallId,
|
|
658
|
-
agentName: data.agentName,
|
|
659
|
-
agentDisplayName: data.agentDisplayName,
|
|
660
|
-
error: data.error,
|
|
661
|
-
}, item.turnId);
|
|
662
|
-
})
|
|
663
|
-
: () => { };
|
|
664
|
-
// Always persist SDK subagent dispatches to agent_tasks so Workers tab shows them.
|
|
665
|
-
// Also emit turn events unconditionally alongside existing callback path (#130).
|
|
666
|
-
const unsubTurnToolStart = session.on("tool.execution_start", (event) => {
|
|
667
|
-
const data = event.data;
|
|
668
|
-
// Skip nested subagent tool calls (handled via subagent.started/completed)
|
|
669
|
-
const part = {
|
|
670
|
-
type: "tool-call",
|
|
671
|
-
toolCallId: String(data.toolCallId ?? ""),
|
|
672
|
-
toolName: String(data.toolName ?? "unknown"),
|
|
673
|
-
mcpServerName: typeof data.mcpServerName === "string" ? data.mcpServerName : undefined,
|
|
674
|
-
arguments: data.arguments,
|
|
675
|
-
status: "running",
|
|
676
|
-
};
|
|
677
|
-
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part });
|
|
678
|
-
});
|
|
679
|
-
const unsubTurnReasoning = session.on("assistant.reasoning_delta", (event) => {
|
|
680
|
-
const part = {
|
|
681
|
-
type: "thinking",
|
|
682
|
-
reasoningId: event.data.reasoningId,
|
|
683
|
-
text: event.data.deltaContent,
|
|
684
|
-
active: true,
|
|
685
|
-
};
|
|
686
|
-
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part });
|
|
687
|
-
});
|
|
688
518
|
const db = getDb();
|
|
689
519
|
// Set of task IDs for subagents spawned in THIS turn — used to filter nested tool events.
|
|
690
520
|
const activeSubagentTaskIds = new Set();
|
|
691
|
-
|
|
692
|
-
|
|
521
|
+
let unsubSpawnCapture = () => { };
|
|
522
|
+
let unsubToolDone = () => { };
|
|
523
|
+
let unsubToolStart = () => { };
|
|
524
|
+
let unsubReasoning = () => { };
|
|
525
|
+
let unsubSubStart = () => { };
|
|
526
|
+
let unsubSubDone = () => { };
|
|
527
|
+
let unsubSubFail = () => { };
|
|
528
|
+
let unsubTurnToolStart = () => { };
|
|
529
|
+
let unsubTurnReasoning = () => { };
|
|
530
|
+
let unsubSubStartDb = () => { };
|
|
531
|
+
let unsubSubDoneDb = () => { };
|
|
532
|
+
let unsubSubFailDb = () => { };
|
|
533
|
+
let unsubNestedToolStart = () => { };
|
|
534
|
+
let unsubNestedToolDone = () => { };
|
|
535
|
+
let unsubDelta = () => { };
|
|
536
|
+
try {
|
|
537
|
+
// Unconditional capture — must fire even when onActivity is absent so the DB handler can resolve names.
|
|
538
|
+
unsubSpawnCapture = session.on("tool.execution_start", (event) => {
|
|
693
539
|
const data = event.data;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
: data.agentDescription || data.agentDisplayName || `Agent dispatch: ${agentSlug}`).slice(0, 500);
|
|
701
|
-
const prompt = typeof spawnArgs?.prompt === "string" ? spawnArgs.prompt : null;
|
|
702
|
-
db.prepare(`INSERT OR IGNORE INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
|
|
703
|
-
VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(data.toolCallId, agentSlug, description, prompt, item.sourceChannel || null, sessionKey);
|
|
704
|
-
if (prompt) {
|
|
705
|
-
db.prepare(`UPDATE agent_tasks SET prompt = COALESCE(prompt, ?) WHERE task_id = ?`).run(prompt, data.toolCallId);
|
|
540
|
+
if (data.toolCallId) {
|
|
541
|
+
toolStartDetails.set(data.toolCallId, {
|
|
542
|
+
toolName: String(data.toolName ?? "unknown"),
|
|
543
|
+
mcpServerName: typeof data.mcpServerName === "string" ? data.mcpServerName : undefined,
|
|
544
|
+
arguments: data.arguments,
|
|
545
|
+
});
|
|
706
546
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
type: "subagent",
|
|
718
|
-
toolCallId: String(data.toolCallId ?? ""),
|
|
719
|
-
agentName: data.agentName ?? agentSlug,
|
|
720
|
-
agentDisplayName: data.agentDisplayName ?? agentSlug,
|
|
721
|
-
agentDescription: description,
|
|
722
|
-
agentSlug,
|
|
723
|
-
status: "running",
|
|
724
|
-
};
|
|
725
|
-
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part: subPart });
|
|
726
|
-
}
|
|
727
|
-
catch { /* non-fatal */ }
|
|
728
|
-
});
|
|
729
|
-
const unsubSubDoneDb = session.on("subagent.completed", (event) => {
|
|
730
|
-
try {
|
|
731
|
-
const doneData = event.data;
|
|
732
|
-
const taskId = String(doneData.toolCallId ?? "");
|
|
733
|
-
const finalResult = typeof doneData.result?.detailedContent === "string"
|
|
734
|
-
? doneData.result.detailedContent
|
|
735
|
-
: typeof doneData.result?.content === "string"
|
|
736
|
-
? doneData.result.content
|
|
737
|
-
: null;
|
|
738
|
-
spawnArgsMap.delete(taskId);
|
|
739
|
-
activeSubagentTaskIds.delete(taskId);
|
|
740
|
-
db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(finalResult?.slice(0, 10000) ?? null, taskId);
|
|
741
|
-
if (finalResult) {
|
|
742
|
-
void memoryCoordinator?.onAgentTaskComplete(taskId, finalResult).catch((error) => {
|
|
743
|
-
log.error({ err: error, taskId }, "memory.eot.error");
|
|
547
|
+
if (data.toolName === "task" && data.toolCallId) {
|
|
548
|
+
const args = (data.arguments ?? {});
|
|
549
|
+
spawnArgsMap.set(data.toolCallId, {
|
|
550
|
+
name: typeof args.name === "string" ? args.name : undefined,
|
|
551
|
+
description: typeof args.description === "string" ? args.description : undefined,
|
|
552
|
+
prompt: typeof args.prompt === "string"
|
|
553
|
+
? args.prompt
|
|
554
|
+
: typeof args.task === "string"
|
|
555
|
+
? args.task
|
|
556
|
+
: undefined,
|
|
744
557
|
});
|
|
745
558
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
559
|
+
});
|
|
560
|
+
unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
561
|
+
toolCallExecuted = true;
|
|
562
|
+
toolCallCount++;
|
|
563
|
+
const data = event.data;
|
|
564
|
+
const result = data.result;
|
|
565
|
+
const resultPreview = typeof result?.content === "string" ? result.content.slice(0, 400) : undefined;
|
|
566
|
+
const detailedContent = typeof result?.detailedContent === "string"
|
|
567
|
+
? result.detailedContent
|
|
568
|
+
: typeof result?.content === "string"
|
|
569
|
+
? result.content
|
|
570
|
+
: undefined;
|
|
571
|
+
const toolCallId = String(data.toolCallId ?? "");
|
|
572
|
+
const startDetails = toolStartDetails.get(toolCallId);
|
|
573
|
+
const completionToolName = data.toolName;
|
|
574
|
+
if (item.onActivity) {
|
|
575
|
+
item.onActivity({
|
|
576
|
+
kind: "tool_complete",
|
|
577
|
+
toolCallId: data.toolCallId,
|
|
578
|
+
success: data.success,
|
|
579
|
+
resultPreview,
|
|
580
|
+
detailedContent,
|
|
581
|
+
}, item.turnId);
|
|
582
|
+
}
|
|
583
|
+
const toolPart = {
|
|
584
|
+
type: "tool-call",
|
|
585
|
+
toolCallId,
|
|
586
|
+
toolName: typeof completionToolName === "string" && completionToolName.length > 0
|
|
587
|
+
? completionToolName
|
|
588
|
+
: (startDetails?.toolName ?? "unknown"),
|
|
589
|
+
mcpServerName: startDetails?.mcpServerName,
|
|
590
|
+
arguments: startDetails?.arguments,
|
|
591
|
+
status: data.success !== false ? "done" : "failed",
|
|
592
|
+
resultPreview,
|
|
593
|
+
detailedContent,
|
|
762
594
|
};
|
|
763
|
-
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part:
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
595
|
+
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part: toolPart });
|
|
596
|
+
if (toolCallId) {
|
|
597
|
+
toolStartDetails.delete(toolCallId);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
unsubToolStart = item.onActivity
|
|
601
|
+
? session.on("tool.execution_start", (event) => {
|
|
602
|
+
const data = event.data;
|
|
603
|
+
item.onActivity({
|
|
604
|
+
kind: "tool_start",
|
|
605
|
+
toolCallId: data.toolCallId,
|
|
606
|
+
toolName: data.toolName,
|
|
607
|
+
mcpServerName: data.mcpServerName,
|
|
608
|
+
arguments: data.arguments,
|
|
609
|
+
}, item.turnId);
|
|
610
|
+
})
|
|
611
|
+
: () => { };
|
|
612
|
+
unsubReasoning = item.onActivity
|
|
613
|
+
? session.on("assistant.reasoning_delta", (event) => {
|
|
614
|
+
item.onActivity({
|
|
615
|
+
kind: "thinking_delta",
|
|
616
|
+
reasoningId: event.data.reasoningId,
|
|
617
|
+
deltaContent: event.data.deltaContent,
|
|
618
|
+
}, item.turnId);
|
|
619
|
+
})
|
|
620
|
+
: () => { };
|
|
621
|
+
unsubSubStart = item.onActivity
|
|
622
|
+
? session.on("subagent.started", (event) => {
|
|
623
|
+
const data = event.data;
|
|
624
|
+
const spawnArgs = spawnArgsMap.get(data.toolCallId);
|
|
625
|
+
const agentSlug = (typeof spawnArgs?.name === "string" ? spawnArgs.name : (data.agentName || "unknown"))
|
|
626
|
+
.toLowerCase()
|
|
627
|
+
.replace(/\s+/g, "-");
|
|
628
|
+
const resolvedDescription = (typeof spawnArgs?.description === "string"
|
|
629
|
+
? spawnArgs.description
|
|
630
|
+
: data.agentDescription || data.agentDisplayName || `Agent dispatch: ${agentSlug}`).slice(0, 500);
|
|
631
|
+
item.onActivity({
|
|
632
|
+
kind: "subagent_started",
|
|
633
|
+
toolCallId: data.toolCallId,
|
|
634
|
+
agentName: data.agentName,
|
|
635
|
+
agentDisplayName: data.agentDisplayName,
|
|
636
|
+
agentDescription: resolvedDescription,
|
|
637
|
+
agentSlug,
|
|
638
|
+
}, item.turnId);
|
|
639
|
+
})
|
|
640
|
+
: () => { };
|
|
641
|
+
unsubSubDone = item.onActivity
|
|
642
|
+
? session.on("subagent.completed", (event) => {
|
|
643
|
+
const data = event.data;
|
|
644
|
+
item.onActivity({
|
|
645
|
+
kind: "subagent_completed",
|
|
646
|
+
toolCallId: data.toolCallId,
|
|
647
|
+
agentName: data.agentName,
|
|
648
|
+
agentDisplayName: data.agentDisplayName,
|
|
649
|
+
durationMs: data.durationMs,
|
|
650
|
+
}, item.turnId);
|
|
651
|
+
})
|
|
652
|
+
: () => { };
|
|
653
|
+
unsubSubFail = item.onActivity
|
|
654
|
+
? session.on("subagent.failed", (event) => {
|
|
655
|
+
const data = event.data;
|
|
656
|
+
item.onActivity({
|
|
657
|
+
kind: "subagent_failed",
|
|
658
|
+
toolCallId: data.toolCallId,
|
|
659
|
+
agentName: data.agentName,
|
|
660
|
+
agentDisplayName: data.agentDisplayName,
|
|
661
|
+
error: data.error,
|
|
662
|
+
}, item.turnId);
|
|
663
|
+
})
|
|
664
|
+
: () => { };
|
|
665
|
+
unsubTurnToolStart = session.on("tool.execution_start", (event) => {
|
|
769
666
|
const data = event.data;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
db.prepare(`UPDATE agent_tasks SET status = 'error', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(data.error || "Subagent failed", data.toolCallId);
|
|
773
|
-
const taskRow = db.prepare(`SELECT agent_slug FROM agent_tasks WHERE task_id = ?`).get(data.toolCallId);
|
|
774
|
-
void agentEventBus.emit({
|
|
775
|
-
type: "session:error",
|
|
776
|
-
sessionId: data.toolCallId,
|
|
777
|
-
agentName: taskRow?.agent_slug,
|
|
778
|
-
payload: { agentName: taskRow?.agent_slug ?? "", error: data.error ?? "Subagent failed" },
|
|
779
|
-
timestamp: new Date(),
|
|
780
|
-
});
|
|
781
|
-
// Emit turn:delta with subagent failed part (coexistence — #130)
|
|
782
|
-
const failPart = {
|
|
783
|
-
type: "subagent",
|
|
667
|
+
const part = {
|
|
668
|
+
type: "tool-call",
|
|
784
669
|
toolCallId: String(data.toolCallId ?? ""),
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
670
|
+
toolName: String(data.toolName ?? "unknown"),
|
|
671
|
+
mcpServerName: typeof data.mcpServerName === "string" ? data.mcpServerName : undefined,
|
|
672
|
+
arguments: data.arguments,
|
|
673
|
+
status: "running",
|
|
789
674
|
};
|
|
790
|
-
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
675
|
+
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part });
|
|
676
|
+
});
|
|
677
|
+
unsubTurnReasoning = session.on("assistant.reasoning_delta", (event) => {
|
|
678
|
+
const part = {
|
|
679
|
+
type: "thinking",
|
|
680
|
+
reasoningId: event.data.reasoningId,
|
|
681
|
+
text: event.data.deltaContent,
|
|
682
|
+
active: true,
|
|
683
|
+
};
|
|
684
|
+
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part });
|
|
685
|
+
});
|
|
686
|
+
unsubSubStartDb = session.on("subagent.started", (event) => {
|
|
687
|
+
try {
|
|
688
|
+
const data = event.data;
|
|
689
|
+
const spawnArgs = spawnArgsMap.get(data.toolCallId);
|
|
690
|
+
const agentSlug = (typeof spawnArgs?.name === "string" ? spawnArgs.name : (data.agentName || "unknown"))
|
|
691
|
+
.toLowerCase()
|
|
692
|
+
.replace(/\s+/g, "-");
|
|
693
|
+
const description = (typeof spawnArgs?.description === "string"
|
|
694
|
+
? spawnArgs.description
|
|
695
|
+
: data.agentDescription || data.agentDisplayName || `Agent dispatch: ${agentSlug}`).slice(0, 500);
|
|
696
|
+
const prompt = typeof spawnArgs?.prompt === "string" ? spawnArgs.prompt : null;
|
|
697
|
+
db.prepare(`INSERT OR IGNORE INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
|
|
698
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(data.toolCallId, agentSlug, description, prompt, item.sourceChannel || null, sessionKey);
|
|
699
|
+
if (prompt) {
|
|
700
|
+
db.prepare(`UPDATE agent_tasks SET prompt = COALESCE(prompt, ?) WHERE task_id = ?`).run(prompt, data.toolCallId);
|
|
701
|
+
}
|
|
702
|
+
activeSubagentTaskIds.add(data.toolCallId);
|
|
703
|
+
void agentEventBus.emit({
|
|
704
|
+
type: "session:created",
|
|
705
|
+
sessionId: data.toolCallId,
|
|
706
|
+
agentName: agentSlug,
|
|
707
|
+
payload: { agentName: agentSlug, priority: "normal" },
|
|
708
|
+
timestamp: new Date(),
|
|
709
|
+
});
|
|
710
|
+
const subPart = {
|
|
711
|
+
type: "subagent",
|
|
712
|
+
toolCallId: String(data.toolCallId ?? ""),
|
|
713
|
+
agentName: data.agentName ?? agentSlug,
|
|
714
|
+
agentDisplayName: data.agentDisplayName ?? agentSlug,
|
|
715
|
+
agentDescription: description,
|
|
716
|
+
agentSlug,
|
|
717
|
+
status: "running",
|
|
718
|
+
};
|
|
719
|
+
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part: subPart });
|
|
720
|
+
}
|
|
721
|
+
catch { /* non-fatal */ }
|
|
722
|
+
});
|
|
723
|
+
unsubSubDoneDb = session.on("subagent.completed", (event) => {
|
|
724
|
+
try {
|
|
725
|
+
const doneData = event.data;
|
|
726
|
+
const taskId = String(doneData.toolCallId ?? "");
|
|
727
|
+
const finalResult = typeof doneData.result?.detailedContent === "string"
|
|
728
|
+
? doneData.result.detailedContent
|
|
729
|
+
: typeof doneData.result?.content === "string"
|
|
730
|
+
? doneData.result.content
|
|
731
|
+
: null;
|
|
732
|
+
spawnArgsMap.delete(taskId);
|
|
733
|
+
activeSubagentTaskIds.delete(taskId);
|
|
734
|
+
if (finalResult && finalResult.length > 10_000) {
|
|
735
|
+
log.warn({ taskId, length: finalResult.length, limit: 10_000 }, "subagent result truncated before persistence");
|
|
736
|
+
}
|
|
737
|
+
db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(finalResult?.slice(0, 10000) ?? null, taskId);
|
|
738
|
+
if (finalResult) {
|
|
739
|
+
void memoryCoordinator?.onAgentTaskComplete(taskId, finalResult).catch((error) => {
|
|
740
|
+
log.error({ err: error, taskId }, "memory.eot.error");
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const taskRow = db.prepare(`SELECT agent_slug FROM agent_tasks WHERE task_id = ?`).get(taskId);
|
|
744
|
+
void agentEventBus.emit({
|
|
745
|
+
type: "session:destroyed",
|
|
746
|
+
sessionId: taskId,
|
|
747
|
+
agentName: taskRow?.agent_slug,
|
|
748
|
+
payload: { agentName: taskRow?.agent_slug ?? "", reason: "complete" },
|
|
749
|
+
timestamp: new Date(),
|
|
750
|
+
});
|
|
751
|
+
const donePart = {
|
|
752
|
+
type: "subagent",
|
|
753
|
+
toolCallId: String(doneData.toolCallId ?? ""),
|
|
754
|
+
agentName: doneData.agentName ?? taskRow?.agent_slug ?? "agent",
|
|
755
|
+
agentDisplayName: doneData.agentDisplayName ?? taskRow?.agent_slug ?? "agent",
|
|
756
|
+
status: "done",
|
|
757
|
+
durationMs: typeof doneData.durationMs === "number" ? doneData.durationMs : undefined,
|
|
758
|
+
};
|
|
759
|
+
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part: donePart });
|
|
760
|
+
}
|
|
761
|
+
catch { /* non-fatal */ }
|
|
762
|
+
});
|
|
763
|
+
unsubSubFailDb = session.on("subagent.failed", (event) => {
|
|
764
|
+
try {
|
|
765
|
+
const data = event.data;
|
|
766
|
+
spawnArgsMap.delete(data.toolCallId);
|
|
767
|
+
activeSubagentTaskIds.delete(data.toolCallId);
|
|
768
|
+
db.prepare(`UPDATE agent_tasks SET status = 'error', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(data.error || "Subagent failed", data.toolCallId);
|
|
769
|
+
const taskRow = db.prepare(`SELECT agent_slug FROM agent_tasks WHERE task_id = ?`).get(data.toolCallId);
|
|
770
|
+
void agentEventBus.emit({
|
|
771
|
+
type: "session:error",
|
|
772
|
+
sessionId: data.toolCallId,
|
|
773
|
+
agentName: taskRow?.agent_slug,
|
|
774
|
+
payload: { agentName: taskRow?.agent_slug ?? "", error: data.error ?? "Subagent failed" },
|
|
775
|
+
timestamp: new Date(),
|
|
776
|
+
});
|
|
777
|
+
const failPart = {
|
|
778
|
+
type: "subagent",
|
|
779
|
+
toolCallId: String(data.toolCallId ?? ""),
|
|
780
|
+
agentName: data.agentName ?? taskRow?.agent_slug ?? "agent",
|
|
781
|
+
agentDisplayName: data.agentDisplayName ?? taskRow?.agent_slug ?? "agent",
|
|
782
|
+
status: "failed",
|
|
783
|
+
error: data.error,
|
|
784
|
+
};
|
|
785
|
+
emitTurnEvent(sessionKey, { type: "turn:delta", turnId: item.turnId, sessionKey, part: failPart });
|
|
786
|
+
}
|
|
787
|
+
catch { /* non-fatal */ }
|
|
788
|
+
});
|
|
789
|
+
unsubNestedToolStart = session.on("tool.execution_start", (event) => {
|
|
790
|
+
try {
|
|
791
|
+
const data = event.data;
|
|
792
|
+
const parentId = data.parentToolCallId;
|
|
793
|
+
if (!parentId || !activeSubagentTaskIds.has(parentId))
|
|
794
|
+
return;
|
|
795
|
+
const toolName = data.toolName ?? null;
|
|
796
|
+
const args = data.arguments ?? {};
|
|
797
|
+
let summary = null;
|
|
798
|
+
if (typeof args.command === "string")
|
|
799
|
+
summary = args.command.slice(0, 120);
|
|
800
|
+
else if (typeof args.path === "string")
|
|
801
|
+
summary = args.path.slice(0, 120);
|
|
802
|
+
else if (typeof args.query === "string")
|
|
803
|
+
summary = args.query.slice(0, 120);
|
|
804
|
+
else if (typeof args.prompt === "string")
|
|
805
|
+
summary = args.prompt.slice(0, 120);
|
|
806
|
+
const ev = appendTaskEvent(parentId, "tool_start", toolName, summary);
|
|
807
|
+
if (ev)
|
|
808
|
+
emitTaskEvent(parentId, ev);
|
|
809
|
+
}
|
|
810
|
+
catch { /* non-fatal */ }
|
|
811
|
+
});
|
|
812
|
+
unsubNestedToolDone = session.on("tool.execution_complete", (event) => {
|
|
813
|
+
try {
|
|
814
|
+
const data = event.data;
|
|
815
|
+
const parentId = data.parentToolCallId;
|
|
816
|
+
if (!parentId || !activeSubagentTaskIds.has(parentId))
|
|
817
|
+
return;
|
|
818
|
+
const success = data.success !== false;
|
|
819
|
+
const resultContent = data.result?.content ?? data.result?.detailedContent;
|
|
820
|
+
const summary = typeof resultContent === "string"
|
|
821
|
+
? (success ? resultContent.slice(0, 120) : `error: ${resultContent.slice(0, 100)}`)
|
|
822
|
+
: (success ? "ok" : "error");
|
|
823
|
+
const ev = appendTaskEvent(parentId, "tool_complete", null, summary);
|
|
824
|
+
if (ev)
|
|
825
|
+
emitTaskEvent(parentId, ev);
|
|
826
|
+
}
|
|
827
|
+
catch { /* non-fatal */ }
|
|
828
|
+
});
|
|
829
|
+
unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
830
|
+
if (toolCallExecuted && accumulated.length > 0 && !accumulated.endsWith("\n")) {
|
|
831
|
+
accumulated += "\n";
|
|
832
|
+
}
|
|
833
|
+
toolCallExecuted = false;
|
|
834
|
+
const delta = event.data.deltaContent;
|
|
835
|
+
accumulated += delta;
|
|
836
|
+
item.callback(accumulated, false, item.turnId);
|
|
837
|
+
emitTurnEvent(sessionKey, {
|
|
838
|
+
type: "turn:delta",
|
|
839
|
+
turnId: item.turnId,
|
|
840
|
+
sessionKey,
|
|
841
|
+
part: { type: "text", text: delta },
|
|
842
|
+
});
|
|
853
843
|
});
|
|
854
|
-
});
|
|
855
|
-
try {
|
|
856
844
|
if (warningBlock) {
|
|
857
845
|
accumulated = warningBlock;
|
|
858
846
|
item.callback(accumulated, false, item.turnId);
|
|
@@ -1008,9 +996,12 @@ function isRecoverableError(err) {
|
|
|
1008
996
|
return false;
|
|
1009
997
|
return /disconnect|connection|EPIPE|ECONNRESET|ECONNREFUSED|socket|closed|ENOENT|spawn|not found|expired|stale/i.test(msg);
|
|
1010
998
|
}
|
|
1011
|
-
|
|
999
|
+
function reconnectDelayMs(attempt) {
|
|
1000
|
+
return Math.min(RECONNECT_BASE_DELAY_MS * (2 ** attempt), RECONNECT_MAX_DELAY_MS);
|
|
1001
|
+
}
|
|
1002
|
+
export function sendToOrchestrator(prompt, source, callback, attachments, onActivity, onQueued, onAdvance, externalTurnId, options) {
|
|
1012
1003
|
updateUserContext(source);
|
|
1013
|
-
|
|
1004
|
+
recordLastAuthenticatedUser(source);
|
|
1014
1005
|
// Use the externally-supplied turnId if provided (POST→SSE path needs the ID
|
|
1015
1006
|
// returned to the client to match every emitted event — Fix 1 root cause).
|
|
1016
1007
|
const turnId = externalTurnId ?? randomUUID();
|
|
@@ -1040,13 +1031,16 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1040
1031
|
const authUser = (source.type === "web" || source.type === "sse-web") ? source.user : undefined;
|
|
1041
1032
|
const authHeader = (source.type === "web" || source.type === "sse-web") ? source.authorizationHeader?.trim() || undefined : undefined;
|
|
1042
1033
|
const emitSessionLifecycle = usesSessionTurnLifecycle(source);
|
|
1034
|
+
const manager = registry?.getOrCreate(sessionKey);
|
|
1043
1035
|
if (emitSessionLifecycle) {
|
|
1044
1036
|
emitTurnEvent(sessionKey, { type: "turn:started", turnId, sessionKey, prompt, attachments });
|
|
1045
1037
|
}
|
|
1046
|
-
const
|
|
1047
|
-
void (async () => {
|
|
1038
|
+
const sendPromise = (async () => {
|
|
1048
1039
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1049
1040
|
try {
|
|
1041
|
+
if (!manager) {
|
|
1042
|
+
throw new Error("Orchestrator is not initialized");
|
|
1043
|
+
}
|
|
1050
1044
|
const finalContent = await new Promise((resolve, reject) => {
|
|
1051
1045
|
manager.enqueue({
|
|
1052
1046
|
prompt: taggedPrompt,
|
|
@@ -1106,7 +1100,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1106
1100
|
return;
|
|
1107
1101
|
}
|
|
1108
1102
|
if (isRecoverableError(err) && attempt < MAX_RETRIES) {
|
|
1109
|
-
const delay =
|
|
1103
|
+
const delay = reconnectDelayMs(attempt);
|
|
1110
1104
|
log.warn({ msg, attempt: attempt + 1, maxRetries: MAX_RETRIES, delayMs: delay }, "Recoverable error, retrying");
|
|
1111
1105
|
await sleep(delay);
|
|
1112
1106
|
try {
|
|
@@ -1124,6 +1118,10 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1124
1118
|
}
|
|
1125
1119
|
}
|
|
1126
1120
|
})();
|
|
1121
|
+
void sendPromise.catch((err) => {
|
|
1122
|
+
log.error({ err, sessionKey, turnId }, "unhandled rejection in sendToOrchestrator");
|
|
1123
|
+
});
|
|
1124
|
+
return Promise.resolve();
|
|
1127
1125
|
}
|
|
1128
1126
|
/**
|
|
1129
1127
|
* Abort the active turn on `sessionKey` and immediately start a new turn with `newPrompt`.
|
|
@@ -1144,8 +1142,9 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1144
1142
|
return sendToOrchestrator(newPrompt, source, callback, attachments, onActivity, undefined, undefined, externalTurnId);
|
|
1145
1143
|
}
|
|
1146
1144
|
updateUserContext(source);
|
|
1147
|
-
|
|
1145
|
+
recordLastAuthenticatedUser(source);
|
|
1148
1146
|
const turnId = externalTurnId ?? randomUUID();
|
|
1147
|
+
const abortedTurnId = manager.currentTurnId;
|
|
1149
1148
|
const sourceLabel = source.type === "background" ? "background" : "web";
|
|
1150
1149
|
const sourceChannel = source.type === "web" ? "web" : undefined;
|
|
1151
1150
|
const authUser = (source.type === "web" || source.type === "sse-web") ? source.user : undefined;
|
|
@@ -1158,7 +1157,7 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1158
1157
|
manager.cancelQueued();
|
|
1159
1158
|
// Enqueue the replacement BEFORE awaiting abort. When sendAndWait resolves on
|
|
1160
1159
|
// abort, the drain loop immediately finds the replacement in the queue — zero gap.
|
|
1161
|
-
|
|
1160
|
+
const interruptPromise = (async () => {
|
|
1162
1161
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1163
1162
|
try {
|
|
1164
1163
|
const finalContent = await new Promise((resolve, reject) => {
|
|
@@ -1170,7 +1169,7 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1170
1169
|
onActivity: onActivity,
|
|
1171
1170
|
turnId,
|
|
1172
1171
|
isInterrupt: true,
|
|
1173
|
-
onInterrupted,
|
|
1172
|
+
onInterrupted: undefined,
|
|
1174
1173
|
sourceChannel,
|
|
1175
1174
|
sessionKey,
|
|
1176
1175
|
authUser,
|
|
@@ -1207,7 +1206,7 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1207
1206
|
if (/cancelled|abort/i.test(msg))
|
|
1208
1207
|
return;
|
|
1209
1208
|
if (isRecoverableError(err) && attempt < MAX_RETRIES) {
|
|
1210
|
-
const delay =
|
|
1209
|
+
const delay = reconnectDelayMs(attempt);
|
|
1211
1210
|
log.warn({ msg, attempt: attempt + 1, maxRetries: MAX_RETRIES, delayMs: delay }, "Recoverable error on interrupt turn, retrying");
|
|
1212
1211
|
await sleep(delay);
|
|
1213
1212
|
try {
|
|
@@ -1222,6 +1221,12 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1222
1221
|
}
|
|
1223
1222
|
}
|
|
1224
1223
|
})();
|
|
1224
|
+
void interruptPromise.catch((err) => {
|
|
1225
|
+
log.error({ err, sessionKey, turnId }, "unhandled rejection in interruptCurrentTurn");
|
|
1226
|
+
});
|
|
1227
|
+
if (abortedTurnId) {
|
|
1228
|
+
onInterrupted?.(abortedTurnId);
|
|
1229
|
+
}
|
|
1225
1230
|
// Abort the in-flight turn AFTER enqueueing the replacement — SDK sends the
|
|
1226
1231
|
// abort RPC; server emits session.idle; sendAndWait resolves; drain loop picks
|
|
1227
1232
|
// up the replacement immediately.
|
|
@@ -1364,6 +1369,7 @@ export function getAgentInfo() {
|
|
|
1364
1369
|
}
|
|
1365
1370
|
/** Clean up on shutdown/restart. */
|
|
1366
1371
|
export async function shutdownAgents() {
|
|
1372
|
+
stopClassifier();
|
|
1367
1373
|
memoryCoordinator?.shutdown();
|
|
1368
1374
|
memoryCoordinator = undefined;
|
|
1369
1375
|
if (!registry) {
|