chapterhouse 0.3.0 → 0.3.2
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 +72 -0
- package/dist/api/auth.js +4 -3
- package/dist/api/auth.test.js +27 -39
- package/dist/api/server.js +3 -0
- package/dist/api/team.js +4 -2
- package/dist/cli.js +8 -2
- package/dist/copilot/episode-writer.js +4 -2
- package/dist/copilot/orchestrator.js +312 -356
- package/dist/copilot/orchestrator.test.js +40 -1
- package/dist/copilot/session-manager.js +307 -0
- package/dist/copilot/session-manager.test.js +292 -0
- package/dist/copilot/system-message.js +2 -1
- package/dist/copilot/system-message.test.js +8 -0
- package/dist/daemon-install.js +62 -3
- package/dist/daemon-install.test.js +35 -0
- package/dist/daemon.js +21 -3
- package/dist/integrations/teams-notify.js +3 -1
- package/dist/squad/index.js +1 -0
- package/dist/squad/init-cli.js +109 -0
- package/dist/squad/init.js +395 -0
- package/dist/squad/init.test.js +351 -0
- package/dist/squad/mirror.js +4 -2
- package/dist/squad/mirror.scheduler.js +9 -7
- package/dist/version.js +7 -0
- package/dist/wiki/team-sync.js +3 -1
- package/package.json +2 -3
- package/web/dist/assets/index-Dpt-MCe8.css +10 -0
- package/web/dist/assets/index-NmxVWGY1.js +205 -0
- package/web/dist/assets/index-NmxVWGY1.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CxT9905O.css +0 -10
- package/web/dist/assets/index-DI3rnGm-.js +0 -142
- package/web/dist/assets/index-DI3rnGm-.js.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { approveAll } from "@github/copilot-sdk";
|
|
2
3
|
import { createTools } from "./tools.js";
|
|
3
4
|
import { getOrchestratorSystemMessage } from "./system-message.js";
|
|
5
|
+
import { CHAPTERHOUSE_VERSION } from "../version.js";
|
|
4
6
|
import { config, DEFAULT_MODEL } from "../config.js";
|
|
5
7
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
6
8
|
import { getSkillDirectories } from "./skills.js";
|
|
@@ -14,11 +16,8 @@ import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, se
|
|
|
14
16
|
import { normalizeProjectPath, setChannelProject } from "../squad/context.js";
|
|
15
17
|
import { getSquadCoordinatorSystemMessage } from "../squad/charter.js";
|
|
16
18
|
import { childLogger } from "../util/logger.js";
|
|
19
|
+
import { SessionManager, SessionRegistry, SESSION_IDLE_TTL_MS, SESSION_MAX_ACTIVE, } from "./session-manager.js";
|
|
17
20
|
const log = childLogger("orchestrator");
|
|
18
|
-
/**
|
|
19
|
-
* Permission handler for the orchestrator session.
|
|
20
|
-
* Approves all tool requests so @chapterhouse has full access to all tools.
|
|
21
|
-
*/
|
|
22
21
|
const orchestratorPermissionHandler = approveAll;
|
|
23
22
|
const MAX_RETRIES = 3;
|
|
24
23
|
const RECONNECT_DELAYS_MS = [1_000, 3_000, 10_000];
|
|
@@ -33,61 +32,58 @@ let proactiveNotifyFn;
|
|
|
33
32
|
export function setProactiveNotify(fn) {
|
|
34
33
|
proactiveNotifyFn = fn;
|
|
35
34
|
}
|
|
35
|
+
const turnContextStorage = new AsyncLocalStorage();
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Module-level state (not per-session)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
36
39
|
let copilotClient;
|
|
37
40
|
let healthCheckTimer;
|
|
38
41
|
let currentUserContext;
|
|
42
|
+
/**
|
|
43
|
+
* Last-seen auth context — persists after a turn completes so that callers
|
|
44
|
+
* which inspect it outside of an active turn (e.g. /api/cancel) still see the
|
|
45
|
+
* most recent values. Tools that run DURING a turn should use the per-turn
|
|
46
|
+
* AsyncLocalStorage context for safety in concurrent-session scenarios.
|
|
47
|
+
*/
|
|
39
48
|
let currentAuthenticatedUser;
|
|
40
49
|
let currentAuthorizationHeader;
|
|
41
|
-
// Router state
|
|
42
|
-
let recentTiers = [];
|
|
43
50
|
let lastRouteResult;
|
|
44
51
|
export function getLastRouteResult() {
|
|
45
52
|
return lastRouteResult;
|
|
46
53
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// SessionRegistry — the single owner of all per-session orchestrators
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
let registry;
|
|
58
|
+
function buildRegistry() {
|
|
59
|
+
return new SessionRegistry({ idleTtlMs: SESSION_IDLE_TTL_MS, maxActive: SESSION_MAX_ACTIVE }, (sessionKey) => new SessionManager(sessionKey, processItem, createOrResumeSession));
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Context getters — exported for tools.ts and server.ts
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
53
64
|
export function getCurrentSessionKey() {
|
|
54
|
-
return
|
|
65
|
+
return turnContextStorage.getStore()?.sessionKey ?? "default";
|
|
55
66
|
}
|
|
56
|
-
const messageQueue = [];
|
|
57
|
-
let processing = false;
|
|
58
|
-
let currentCallback;
|
|
59
|
-
/** The channel currently being processed — tools use this to tag new workers. */
|
|
60
|
-
let currentSourceChannel;
|
|
61
|
-
/** Get the channel that originated the message currently being processed. */
|
|
62
67
|
export function getCurrentSourceChannel() {
|
|
63
|
-
return
|
|
68
|
+
return turnContextStorage.getStore()?.sourceChannel;
|
|
64
69
|
}
|
|
65
|
-
let currentChannelKey;
|
|
66
70
|
export function getCurrentChannelKey() {
|
|
67
|
-
return
|
|
71
|
+
return turnContextStorage.getStore()?.channelKey;
|
|
68
72
|
}
|
|
69
|
-
/**
|
|
70
|
-
* The activity callback for the message currently being processed. Tool handlers
|
|
71
|
-
* (notably `delegate_to_agent`) read this to forward child-session events back
|
|
72
|
-
* to the parent's SSE connection.
|
|
73
|
-
*/
|
|
74
|
-
let currentActivityCallback;
|
|
75
73
|
export function getCurrentActivityCallback() {
|
|
76
|
-
return
|
|
74
|
+
return turnContextStorage.getStore()?.activityCallback;
|
|
77
75
|
}
|
|
78
76
|
export function getCurrentAuthenticatedUser() {
|
|
79
|
-
return currentAuthenticatedUser;
|
|
77
|
+
return turnContextStorage.getStore()?.authUser ?? currentAuthenticatedUser;
|
|
80
78
|
}
|
|
81
79
|
export function getLastAuthenticatedUser() {
|
|
82
80
|
const raw = getState(LAST_AUTHENTICATED_USER_KEY);
|
|
83
|
-
if (!raw)
|
|
81
|
+
if (!raw)
|
|
84
82
|
return undefined;
|
|
85
|
-
}
|
|
86
83
|
try {
|
|
87
84
|
const parsed = JSON.parse(raw);
|
|
88
|
-
if (!parsed?.id || !parsed?.name || !parsed?.email || !parsed?.role)
|
|
85
|
+
if (!parsed?.id || !parsed?.name || !parsed?.email || !parsed?.role)
|
|
89
86
|
return undefined;
|
|
90
|
-
}
|
|
91
87
|
return parsed;
|
|
92
88
|
}
|
|
93
89
|
catch {
|
|
@@ -95,8 +91,11 @@ export function getLastAuthenticatedUser() {
|
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
export function getCurrentAuthorizationHeader() {
|
|
98
|
-
return currentAuthorizationHeader;
|
|
94
|
+
return turnContextStorage.getStore()?.authHeader ?? currentAuthorizationHeader;
|
|
99
95
|
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Internal helpers
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
100
99
|
function getSessionConfig() {
|
|
101
100
|
const tools = createTools({
|
|
102
101
|
client: copilotClient,
|
|
@@ -118,17 +117,16 @@ function sameUserContext(a, b) {
|
|
|
118
117
|
return a?.name === b?.name && a?.role === b?.role;
|
|
119
118
|
}
|
|
120
119
|
function updateUserContext(source) {
|
|
121
|
-
if (source.type !== "web")
|
|
120
|
+
if (source.type !== "web")
|
|
122
121
|
return;
|
|
123
|
-
}
|
|
124
122
|
const nextContext = source.user
|
|
125
123
|
? { name: source.user.name, role: source.user.role }
|
|
126
124
|
: undefined;
|
|
127
|
-
if (sameUserContext(currentUserContext, nextContext))
|
|
125
|
+
if (sameUserContext(currentUserContext, nextContext))
|
|
128
126
|
return;
|
|
129
|
-
}
|
|
130
127
|
currentUserContext = nextContext;
|
|
131
|
-
|
|
128
|
+
// Invalidate the default session so it's recreated with the updated system message
|
|
129
|
+
registry?.get("default")?.invalidateSession();
|
|
132
130
|
}
|
|
133
131
|
function updateRequestContext(source) {
|
|
134
132
|
if (source.type !== "web") {
|
|
@@ -142,7 +140,6 @@ function updateRequestContext(source) {
|
|
|
142
140
|
setState(LAST_AUTHENTICATED_USER_KEY, JSON.stringify(source.user));
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
|
-
/** Feed an agent task result into the orchestrator as a new turn. */
|
|
146
143
|
export function feedAgentResult(taskId, agentSlug, result) {
|
|
147
144
|
const prompt = `[Agent task completed] @${agentSlug} finished task ${taskId}:\n\n${result}`;
|
|
148
145
|
const sessionKey = getTaskSessionKey(taskId);
|
|
@@ -155,7 +152,6 @@ export function feedAgentResult(taskId, agentSlug, result) {
|
|
|
155
152
|
function sleep(ms) {
|
|
156
153
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
157
154
|
}
|
|
158
|
-
/** Ensure the SDK client is connected, resetting if necessary. Coalesces concurrent resets. */
|
|
159
155
|
let resetPromise;
|
|
160
156
|
async function ensureClient() {
|
|
161
157
|
if (copilotClient && copilotClient.getState() === "connected") {
|
|
@@ -171,7 +167,6 @@ async function ensureClient() {
|
|
|
171
167
|
}
|
|
172
168
|
return resetPromise;
|
|
173
169
|
}
|
|
174
|
-
/** Start periodic health check that proactively reconnects the client. */
|
|
175
170
|
function startHealthCheck() {
|
|
176
171
|
if (healthCheckTimer)
|
|
177
172
|
return;
|
|
@@ -183,9 +178,10 @@ function startHealthCheck() {
|
|
|
183
178
|
if (state !== "connected") {
|
|
184
179
|
log.info({ state }, "Health check: client not connected, resetting");
|
|
185
180
|
await ensureClient();
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
// Invalidate all cached sessions — they're tied to the old connection
|
|
182
|
+
for (const [, mgr] of registry.getAll()) {
|
|
183
|
+
mgr.invalidateSession();
|
|
184
|
+
}
|
|
189
185
|
}
|
|
190
186
|
}
|
|
191
187
|
catch (err) {
|
|
@@ -193,27 +189,7 @@ function startHealthCheck() {
|
|
|
193
189
|
}
|
|
194
190
|
}, HEALTH_CHECK_INTERVAL_MS);
|
|
195
191
|
}
|
|
196
|
-
/**
|
|
197
|
-
async function ensureOrchestratorSession(sessionKey, projectRoot) {
|
|
198
|
-
const existing = sessionMap.get(sessionKey);
|
|
199
|
-
if (existing)
|
|
200
|
-
return existing;
|
|
201
|
-
// Coalesce concurrent callers for the same key
|
|
202
|
-
const inFlight = sessionCreatePromises.get(sessionKey);
|
|
203
|
-
if (inFlight)
|
|
204
|
-
return inFlight;
|
|
205
|
-
const promise = createOrResumeSession(sessionKey, projectRoot);
|
|
206
|
-
sessionCreatePromises.set(sessionKey, promise);
|
|
207
|
-
try {
|
|
208
|
-
const session = await promise;
|
|
209
|
-
sessionMap.set(sessionKey, session);
|
|
210
|
-
return session;
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
sessionCreatePromises.delete(sessionKey);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/** Internal: actually create or resume a session (not concurrency-safe — use ensureOrchestratorSession). */
|
|
192
|
+
/** Internal: create or resume a CopilotSession. Called by SessionManager.ensureSession(). */
|
|
217
193
|
async function createOrResumeSession(sessionKey, projectRoot) {
|
|
218
194
|
const client = await ensureClient();
|
|
219
195
|
const { tools, mcpServers, skillDirectories } = getSessionConfig();
|
|
@@ -223,7 +199,6 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
223
199
|
backgroundCompactionThreshold: 0.80,
|
|
224
200
|
bufferExhaustionThreshold: 0.95,
|
|
225
201
|
};
|
|
226
|
-
// Build the correct system message for this session mode
|
|
227
202
|
let systemMessageContent;
|
|
228
203
|
if (isProjectSession && projectRoot) {
|
|
229
204
|
systemMessageContent = await getSquadCoordinatorSystemMessage(projectRoot);
|
|
@@ -232,9 +207,9 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
232
207
|
const memorySummary = getWikiSummary();
|
|
233
208
|
systemMessageContent = getOrchestratorSystemMessage({
|
|
234
209
|
...getSystemMessageOptions(memorySummary, isProjectSession ? projectRoot : undefined),
|
|
210
|
+
version: CHAPTERHOUSE_VERSION,
|
|
235
211
|
});
|
|
236
212
|
}
|
|
237
|
-
// Try to resume from copilot_sessions; fall back to legacy max_state key for the default session
|
|
238
213
|
const stored = getCopilotSession(sessionKey);
|
|
239
214
|
const savedSessionId = stored?.copilotSessionId ?? (sessionKey === "default" ? getState(ORCHESTRATOR_SESSION_KEY) : undefined);
|
|
240
215
|
if (savedSessionId) {
|
|
@@ -253,7 +228,9 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
253
228
|
});
|
|
254
229
|
log.info({ sessionKey }, "Session resumed successfully");
|
|
255
230
|
upsertCopilotSession(sessionKey, isProjectSession ? "project" : "default", session.sessionId, projectRoot, config.copilotModel);
|
|
256
|
-
|
|
231
|
+
const mgr = registry?.get(sessionKey);
|
|
232
|
+
if (mgr)
|
|
233
|
+
mgr.currentModel = config.copilotModel;
|
|
257
234
|
return session;
|
|
258
235
|
}
|
|
259
236
|
catch (err) {
|
|
@@ -262,7 +239,6 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
262
239
|
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
263
240
|
}
|
|
264
241
|
}
|
|
265
|
-
// Create a fresh session
|
|
266
242
|
log.info({ sessionKey }, "Creating new session");
|
|
267
243
|
const session = await client.createSession({
|
|
268
244
|
model: config.copilotModel,
|
|
@@ -277,20 +253,25 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
277
253
|
});
|
|
278
254
|
log.info({ sessionKey, sessionId: session.sessionId.slice(0, 8) }, "Session created");
|
|
279
255
|
upsertCopilotSession(sessionKey, isProjectSession ? "project" : "default", session.sessionId, projectRoot, config.copilotModel);
|
|
280
|
-
// Backward compat: also persist the default session to the legacy state key
|
|
281
256
|
if (sessionKey === "default")
|
|
282
257
|
setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
|
|
283
|
-
|
|
258
|
+
const mgr = registry?.get(sessionKey);
|
|
259
|
+
if (mgr)
|
|
260
|
+
mgr.currentModel = config.copilotModel;
|
|
284
261
|
return session;
|
|
285
262
|
}
|
|
286
263
|
export async function initOrchestrator(client) {
|
|
287
264
|
copilotClient = client;
|
|
265
|
+
// (Re-)create the registry — supports multiple initOrchestrator calls in tests
|
|
266
|
+
if (registry) {
|
|
267
|
+
await registry.shutdown();
|
|
268
|
+
}
|
|
269
|
+
registry = buildRegistry();
|
|
270
|
+
registry.startEvictionTimer();
|
|
288
271
|
const { mcpServers, skillDirectories } = getSessionConfig();
|
|
289
|
-
// Initialize agent system
|
|
290
272
|
ensureDefaultAgents();
|
|
291
273
|
const agents = loadAgents();
|
|
292
274
|
log.info({ count: agents.length, agents: agents.map((a) => `@${a.slug}`) }, "Agents loaded");
|
|
293
|
-
// Validate configured model against available models
|
|
294
275
|
try {
|
|
295
276
|
const models = await client.listModels();
|
|
296
277
|
const configured = config.copilotModel;
|
|
@@ -307,255 +288,240 @@ export async function initOrchestrator(client) {
|
|
|
307
288
|
log.info({ skillDirectories }, "Skill directories");
|
|
308
289
|
log.info("Persistent session mode — conversation history maintained by SDK");
|
|
309
290
|
startHealthCheck();
|
|
310
|
-
// Eagerly create/resume the default orchestrator session
|
|
311
291
|
try {
|
|
312
|
-
|
|
292
|
+
const defaultManager = registry.getOrCreate("default");
|
|
293
|
+
await defaultManager.ensureSession();
|
|
313
294
|
}
|
|
314
295
|
catch (err) {
|
|
315
296
|
log.error({ err: err instanceof Error ? err.message : err }, "Failed to create initial session (will retry on first message)");
|
|
316
297
|
}
|
|
317
298
|
}
|
|
318
|
-
/** How long to wait for the orchestrator to finish a turn (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
299
|
+
/** How long to wait for the orchestrator to finish a single session turn (30 min default).
|
|
300
|
+
* Override with CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS env var (parsed as integer ms).
|
|
301
|
+
* Applies per-session-turn; concurrent sessions each have their own independent timeout.
|
|
302
|
+
* Part of the 3-layer timing contract — see systemd unit TimeoutStopSec comment. */
|
|
303
|
+
const DEFAULT_ORCHESTRATOR_TIMEOUT_MS = 1_800_000;
|
|
304
|
+
export const ORCHESTRATOR_TIMEOUT_MS = (() => {
|
|
305
|
+
const env = process.env.CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS;
|
|
306
|
+
if (env) {
|
|
307
|
+
const parsed = parseInt(env, 10);
|
|
308
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
309
|
+
return parsed;
|
|
310
|
+
}
|
|
311
|
+
return DEFAULT_ORCHESTRATOR_TIMEOUT_MS;
|
|
312
|
+
})();
|
|
313
|
+
/**
|
|
314
|
+
* Execute a single queued item on its session.
|
|
315
|
+
* Wraps the entire turn in AsyncLocalStorage so all tool handlers (e.g. delegate_to_agent,
|
|
316
|
+
* register_task) see the correct per-session context even when multiple sessions run
|
|
317
|
+
* concurrently. This is the core of the per-session isolation guarantee.
|
|
318
|
+
*/
|
|
319
|
+
async function executeOnSession(manager, item) {
|
|
320
|
+
const { sessionKey } = manager;
|
|
321
|
+
const session = await manager.ensureSession();
|
|
322
|
+
// Update last-seen globals (backwards compat — for callers that inspect after a turn ends)
|
|
323
|
+
currentAuthenticatedUser = item.authUser;
|
|
324
|
+
currentAuthorizationHeader = item.authHeader;
|
|
325
|
+
return turnContextStorage.run({
|
|
326
|
+
sessionKey,
|
|
327
|
+
sourceChannel: item.sourceChannel,
|
|
328
|
+
channelKey: item.channelKey,
|
|
329
|
+
authUser: item.authUser,
|
|
330
|
+
authHeader: item.authHeader,
|
|
331
|
+
activityCallback: item.onActivity,
|
|
332
|
+
}, async () => {
|
|
333
|
+
let accumulated = "";
|
|
334
|
+
let toolCallExecuted = false;
|
|
335
|
+
let toolCallCount = 0;
|
|
336
|
+
const unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
337
|
+
toolCallExecuted = true;
|
|
338
|
+
toolCallCount++;
|
|
339
|
+
if (item.onActivity) {
|
|
340
|
+
const data = event.data;
|
|
341
|
+
const result = data.result;
|
|
342
|
+
const resultPreview = typeof result?.content === "string" ? result.content.slice(0, 400) : undefined;
|
|
343
|
+
const detailedContent = typeof result?.detailedContent === "string"
|
|
344
|
+
? result.detailedContent
|
|
345
|
+
: typeof result?.content === "string"
|
|
346
|
+
? result.content
|
|
347
|
+
: undefined;
|
|
348
|
+
item.onActivity({
|
|
349
|
+
kind: "tool_complete",
|
|
350
|
+
toolCallId: data.toolCallId,
|
|
351
|
+
success: data.success,
|
|
352
|
+
resultPreview,
|
|
353
|
+
detailedContent,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
const unsubToolStart = item.onActivity
|
|
358
|
+
? session.on("tool.execution_start", (event) => {
|
|
359
|
+
const data = event.data;
|
|
360
|
+
item.onActivity({
|
|
361
|
+
kind: "tool_start",
|
|
362
|
+
toolCallId: data.toolCallId,
|
|
363
|
+
toolName: data.toolName,
|
|
364
|
+
mcpServerName: data.mcpServerName,
|
|
365
|
+
arguments: data.arguments,
|
|
366
|
+
});
|
|
367
|
+
})
|
|
368
|
+
: () => { };
|
|
369
|
+
const unsubReasoning = item.onActivity
|
|
370
|
+
? session.on("assistant.reasoning_delta", (event) => {
|
|
371
|
+
item.onActivity({
|
|
372
|
+
kind: "thinking_delta",
|
|
373
|
+
reasoningId: event.data.reasoningId,
|
|
374
|
+
deltaContent: event.data.deltaContent,
|
|
375
|
+
});
|
|
376
|
+
})
|
|
377
|
+
: () => { };
|
|
378
|
+
const unsubSubStart = item.onActivity
|
|
379
|
+
? session.on("subagent.started", (event) => {
|
|
380
|
+
const data = event.data;
|
|
381
|
+
item.onActivity({
|
|
382
|
+
kind: "subagent_started",
|
|
383
|
+
toolCallId: data.toolCallId,
|
|
384
|
+
agentName: data.agentName,
|
|
385
|
+
agentDisplayName: data.agentDisplayName,
|
|
386
|
+
agentDescription: data.agentDescription,
|
|
387
|
+
});
|
|
388
|
+
})
|
|
389
|
+
: () => { };
|
|
390
|
+
const unsubSubDone = item.onActivity
|
|
391
|
+
? session.on("subagent.completed", (event) => {
|
|
392
|
+
const data = event.data;
|
|
393
|
+
item.onActivity({
|
|
394
|
+
kind: "subagent_completed",
|
|
395
|
+
toolCallId: data.toolCallId,
|
|
396
|
+
agentName: data.agentName,
|
|
397
|
+
agentDisplayName: data.agentDisplayName,
|
|
398
|
+
durationMs: data.durationMs,
|
|
399
|
+
});
|
|
400
|
+
})
|
|
401
|
+
: () => { };
|
|
402
|
+
const unsubSubFail = item.onActivity
|
|
403
|
+
? session.on("subagent.failed", (event) => {
|
|
404
|
+
const data = event.data;
|
|
405
|
+
item.onActivity({
|
|
406
|
+
kind: "subagent_failed",
|
|
407
|
+
toolCallId: data.toolCallId,
|
|
408
|
+
agentName: data.agentName,
|
|
409
|
+
agentDisplayName: data.agentDisplayName,
|
|
410
|
+
error: data.error,
|
|
411
|
+
});
|
|
412
|
+
})
|
|
413
|
+
: () => { };
|
|
414
|
+
// Always persist SDK subagent dispatches to agent_tasks so Workers tab shows them.
|
|
415
|
+
const db = getDb();
|
|
416
|
+
const unsubSubStartDb = session.on("subagent.started", (event) => {
|
|
417
|
+
try {
|
|
418
|
+
const data = event.data;
|
|
419
|
+
const agentSlug = (data.agentName || "unknown").toLowerCase().replace(/\s+/g, "-");
|
|
420
|
+
const description = (data.agentDescription || data.agentDisplayName || `Squad dispatch: ${agentSlug}`).slice(0, 500);
|
|
421
|
+
db.prepare(`INSERT OR IGNORE INTO agent_tasks (task_id, agent_slug, description, status, origin_channel, session_key, source) VALUES (?, ?, ?, 'running', ?, ?, 'squad')`).run(data.toolCallId, agentSlug, description, item.sourceChannel || null, sessionKey);
|
|
422
|
+
}
|
|
423
|
+
catch { /* non-fatal */ }
|
|
424
|
+
});
|
|
425
|
+
const unsubSubDoneDb = session.on("subagent.completed", (event) => {
|
|
426
|
+
try {
|
|
427
|
+
db.prepare(`UPDATE agent_tasks SET status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(event.data.toolCallId);
|
|
428
|
+
}
|
|
429
|
+
catch { /* non-fatal */ }
|
|
430
|
+
});
|
|
431
|
+
const unsubSubFailDb = session.on("subagent.failed", (event) => {
|
|
432
|
+
try {
|
|
433
|
+
const data = event.data;
|
|
434
|
+
db.prepare(`UPDATE agent_tasks SET status = 'error', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(data.error || "Subagent failed", data.toolCallId);
|
|
435
|
+
}
|
|
436
|
+
catch { /* non-fatal */ }
|
|
437
|
+
});
|
|
438
|
+
const unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
439
|
+
if (toolCallExecuted && accumulated.length > 0 && !accumulated.endsWith("\n")) {
|
|
440
|
+
accumulated += "\n";
|
|
441
|
+
}
|
|
442
|
+
toolCallExecuted = false;
|
|
443
|
+
accumulated += event.data.deltaContent;
|
|
444
|
+
item.callback(accumulated, false);
|
|
445
|
+
});
|
|
428
446
|
try {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
catch { /* non-fatal */ }
|
|
433
|
-
});
|
|
434
|
-
const unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
435
|
-
// After a tool call completes, ensure a line break separates the text blocks
|
|
436
|
-
// so they don't visually run together in the rendered chat.
|
|
437
|
-
if (toolCallExecuted && accumulated.length > 0 && !accumulated.endsWith("\n")) {
|
|
438
|
-
accumulated += "\n";
|
|
447
|
+
const result = await session.sendAndWait({ prompt: item.prompt, ...(item.attachments && item.attachments.length > 0 ? { attachments: item.attachments } : {}) }, ORCHESTRATOR_TIMEOUT_MS);
|
|
448
|
+
const finalContent = result?.data?.content || accumulated || "(No response)";
|
|
449
|
+
return finalContent;
|
|
439
450
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (/timeout/i.test(msg)) {
|
|
454
|
-
if (accumulated.length > 0) {
|
|
455
|
-
log.warn({ timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000, charCount: accumulated.length }, "Timeout with partial response — returning partial");
|
|
456
|
-
return accumulated;
|
|
451
|
+
catch (err) {
|
|
452
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
453
|
+
if (/timeout/i.test(msg)) {
|
|
454
|
+
if (accumulated.length > 0) {
|
|
455
|
+
log.warn({ sessionKey, timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000, charCount: accumulated.length }, "Timeout with partial response — returning partial");
|
|
456
|
+
return accumulated;
|
|
457
|
+
}
|
|
458
|
+
if (toolCallCount > 0) {
|
|
459
|
+
log.warn({ sessionKey, timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000, toolCallCount }, "Timeout — tool calls ran but no text yet, session still working");
|
|
460
|
+
return "I'm still working on this — I've started processing but it's taking longer than expected. I'll send you the results when I'm done.";
|
|
461
|
+
}
|
|
462
|
+
log.warn({ sessionKey, timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000 }, "Timeout with no activity — session may be stuck");
|
|
463
|
+
return "Sorry, that request timed out before I could start working on it. Try again or break it into smaller pieces?";
|
|
457
464
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
465
|
+
if (/closed|destroy|disposed|invalid|expired|not found/i.test(msg)) {
|
|
466
|
+
log.warn({ sessionKey, msg }, "Session appears dead, will recreate");
|
|
467
|
+
manager.invalidateSession();
|
|
468
|
+
if (sessionKey === "default")
|
|
469
|
+
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
463
470
|
}
|
|
464
|
-
|
|
465
|
-
log.warn({ timeoutSec: ORCHESTRATOR_TIMEOUT_MS / 1000 }, "Timeout with no activity — session may be stuck");
|
|
466
|
-
return "Sorry, that request timed out before I could start working on it. Try again or break it into smaller pieces?";
|
|
471
|
+
throw err;
|
|
467
472
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
473
|
+
finally {
|
|
474
|
+
unsubDelta();
|
|
475
|
+
unsubToolDone();
|
|
476
|
+
unsubToolStart();
|
|
477
|
+
unsubReasoning();
|
|
478
|
+
unsubSubStart();
|
|
479
|
+
unsubSubDone();
|
|
480
|
+
unsubSubFail();
|
|
481
|
+
unsubSubStartDb();
|
|
482
|
+
unsubSubDoneDb();
|
|
483
|
+
unsubSubFailDb();
|
|
475
484
|
}
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
finally {
|
|
479
|
-
unsubDelta();
|
|
480
|
-
unsubToolDone();
|
|
481
|
-
unsubToolStart();
|
|
482
|
-
unsubReasoning();
|
|
483
|
-
unsubSubStart();
|
|
484
|
-
unsubSubDone();
|
|
485
|
-
unsubSubFail();
|
|
486
|
-
unsubSubStartDb();
|
|
487
|
-
unsubSubDoneDb();
|
|
488
|
-
unsubSubFailDb();
|
|
489
|
-
currentCallback = undefined;
|
|
490
|
-
currentActivityCallback = undefined;
|
|
491
|
-
currentProcessingSessionKey = undefined;
|
|
492
|
-
}
|
|
485
|
+
});
|
|
493
486
|
}
|
|
494
|
-
/**
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
487
|
+
/**
|
|
488
|
+
* Process a single queued item: route model, handle @mentions, execute.
|
|
489
|
+
* This is the SessionManager worker — one call per turn, inside the drain loop.
|
|
490
|
+
*/
|
|
491
|
+
async function processItem(item, manager) {
|
|
492
|
+
const { sessionKey } = manager;
|
|
493
|
+
if (item.targetAgent && item.targetAgent !== "chapterhouse") {
|
|
494
|
+
setActiveAgent(item.channelKey || "default", item.targetAgent);
|
|
495
|
+
return executeOnSession(manager, item);
|
|
501
496
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
result = await executeOnSession(sessionKey, item.prompt, item.callback, item.attachments, item.onActivity);
|
|
497
|
+
const currentModel = manager.currentModel ?? config.copilotModel;
|
|
498
|
+
const routeResult = await resolveModel(item.prompt, currentModel, manager.recentTiers);
|
|
499
|
+
if (routeResult.switched) {
|
|
500
|
+
log.info({ model: routeResult.model, tier: routeResult.overrideName || routeResult.tier }, "Auto-routing: switching model");
|
|
501
|
+
config.copilotModel = routeResult.model;
|
|
502
|
+
const existingSession = manager.session;
|
|
503
|
+
if (existingSession) {
|
|
504
|
+
try {
|
|
505
|
+
await existingSession.setModel(routeResult.model);
|
|
506
|
+
manager.currentModel = routeResult.model;
|
|
507
|
+
log.info({ sessionKey }, "Model switched in-place");
|
|
514
508
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
log.info({ model: routeResult.model, tier: routeResult.overrideName || routeResult.tier }, "Auto-routing: switching model");
|
|
521
|
-
config.copilotModel = routeResult.model;
|
|
522
|
-
const existingSession = sessionMap.get(sessionKey);
|
|
523
|
-
if (existingSession) {
|
|
524
|
-
try {
|
|
525
|
-
await existingSession.setModel(routeResult.model);
|
|
526
|
-
sessionModelMap.set(sessionKey, routeResult.model);
|
|
527
|
-
log.info({ sessionKey }, "Model switched in-place");
|
|
528
|
-
}
|
|
529
|
-
catch (err) {
|
|
530
|
-
log.warn({ sessionKey, err: err instanceof Error ? err.message : err }, "setModel() failed, will recreate session");
|
|
531
|
-
sessionMap.delete(sessionKey);
|
|
532
|
-
if (sessionKey === "default")
|
|
533
|
-
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (routeResult.tier) {
|
|
538
|
-
recentTiers.push(routeResult.tier);
|
|
539
|
-
if (recentTiers.length > 5)
|
|
540
|
-
recentTiers = recentTiers.slice(-5);
|
|
541
|
-
}
|
|
542
|
-
lastRouteResult = routeResult;
|
|
543
|
-
result = await executeOnSession(sessionKey, item.prompt, item.callback, item.attachments, item.onActivity);
|
|
509
|
+
catch (err) {
|
|
510
|
+
log.warn({ sessionKey, err: err instanceof Error ? err.message : err }, "setModel() failed, will recreate session");
|
|
511
|
+
manager.invalidateSession();
|
|
512
|
+
if (sessionKey === "default")
|
|
513
|
+
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
544
514
|
}
|
|
545
|
-
item.resolve(result);
|
|
546
515
|
}
|
|
547
|
-
catch (err) {
|
|
548
|
-
item.reject(err);
|
|
549
|
-
}
|
|
550
|
-
currentSourceChannel = undefined;
|
|
551
|
-
currentChannelKey = undefined;
|
|
552
516
|
}
|
|
553
|
-
|
|
517
|
+
if (routeResult.tier) {
|
|
518
|
+
manager.addRecentTier(routeResult.tier);
|
|
519
|
+
}
|
|
520
|
+
lastRouteResult = routeResult;
|
|
521
|
+
return executeOnSession(manager, item);
|
|
554
522
|
}
|
|
555
523
|
function isRecoverableError(err) {
|
|
556
524
|
const msg = err instanceof Error ? err.message : String(err);
|
|
557
|
-
// Timeouts are NOT retryable on a persistent session — the message was already
|
|
558
|
-
// sent and likely processed; re-sending creates "duplicate" responses.
|
|
559
525
|
if (/timeout/i.test(msg))
|
|
560
526
|
return false;
|
|
561
527
|
return /disconnect|connection|EPIPE|ECONNRESET|ECONNREFUSED|socket|closed|ENOENT|spawn|not found|expired|stale/i.test(msg);
|
|
@@ -565,14 +531,10 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
565
531
|
updateRequestContext(source);
|
|
566
532
|
const sourceLabel = source.type === "web" ? "web" : "background";
|
|
567
533
|
logMessage("in", sourceLabel, prompt);
|
|
568
|
-
// Derive the session key: project sessions come from web messages with a projectPath;
|
|
569
|
-
// background completions carry their own sessionKey; everything else is "default".
|
|
570
534
|
let sessionKey;
|
|
571
535
|
if (source.type === "web" && source.projectPath && config.squadEnabled) {
|
|
572
536
|
sessionKey = "project:" + normalizeProjectPath(source.projectPath);
|
|
573
|
-
// Keep the legacy channel-project map in sync for tools that read it
|
|
574
537
|
setChannelProject(source.connectionId, normalizeProjectPath(source.projectPath));
|
|
575
|
-
// Bump last-used timestamp so sidebar can sort by real activity
|
|
576
538
|
bumpProjectLastUsed(normalizeProjectPath(source.projectPath));
|
|
577
539
|
}
|
|
578
540
|
else if (source.type === "background" && source.sessionKey) {
|
|
@@ -582,36 +544,46 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
582
544
|
sessionKey = "default";
|
|
583
545
|
}
|
|
584
546
|
const channelKey = source.type === "web" ? source.connectionId : "default";
|
|
585
|
-
// Pass projectRoot to parseAtMention only for project sessions so the default
|
|
586
|
-
// chat does not get Squad roster injection.
|
|
587
547
|
const projectRoot = sessionKey.startsWith("project:") ? sessionKey.slice("project:".length) : undefined;
|
|
588
|
-
// Parse @mention routing (e.g., "@coder fix the bug" → target "coder")
|
|
589
548
|
const mention = parseAtMention(prompt, projectRoot);
|
|
590
549
|
const targetAgent = mention?.agentSlug;
|
|
591
550
|
const routedPrompt = mention ? mention.message : prompt;
|
|
592
|
-
// Tag the prompt with its source channel
|
|
593
551
|
const taggedPrompt = source.type === "background"
|
|
594
552
|
? routedPrompt
|
|
595
553
|
: `[via ${sourceLabel}] ${routedPrompt}`;
|
|
596
|
-
// Log role: background events are "system", user messages are "user"
|
|
597
554
|
const logRole = source.type === "background" ? "system" : "user";
|
|
598
|
-
// Determine the source channel for agent origin tracking
|
|
599
555
|
const sourceChannel = source.type === "web" ? "web" : undefined;
|
|
600
|
-
//
|
|
556
|
+
// Capture auth context at enqueue time — prevents cross-session contamination
|
|
557
|
+
// when concurrent sessions are processing simultaneously.
|
|
558
|
+
const authUser = source.type === "web" ? source.user : undefined;
|
|
559
|
+
const authHeader = source.type === "web" ? source.authorizationHeader?.trim() || undefined : undefined;
|
|
560
|
+
const manager = registry.getOrCreate(sessionKey);
|
|
601
561
|
void (async () => {
|
|
602
562
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
603
563
|
try {
|
|
604
564
|
const finalContent = await new Promise((resolve, reject) => {
|
|
605
|
-
|
|
606
|
-
|
|
565
|
+
manager.enqueue({
|
|
566
|
+
prompt: taggedPrompt,
|
|
567
|
+
attachments,
|
|
568
|
+
callback,
|
|
569
|
+
// Cast: QueuedMessage.onActivity uses a wide event type to avoid circular
|
|
570
|
+
// type dependencies. orchestrator.ts always passes valid ActivityEvent objects.
|
|
571
|
+
onActivity: onActivity,
|
|
572
|
+
sourceChannel,
|
|
573
|
+
targetAgent,
|
|
574
|
+
channelKey,
|
|
575
|
+
sessionKey,
|
|
576
|
+
authUser,
|
|
577
|
+
authHeader,
|
|
578
|
+
resolve,
|
|
579
|
+
reject,
|
|
580
|
+
});
|
|
607
581
|
});
|
|
608
|
-
// Deliver response to user FIRST, then log best-effort
|
|
609
582
|
callback(finalContent, true);
|
|
610
583
|
try {
|
|
611
584
|
logMessage("out", sourceLabel, finalContent);
|
|
612
585
|
}
|
|
613
586
|
catch { /* best-effort */ }
|
|
614
|
-
// Log both sides of the conversation, scoped to the session
|
|
615
587
|
try {
|
|
616
588
|
logConversation(logRole, prompt, sourceLabel, sessionKey);
|
|
617
589
|
}
|
|
@@ -620,9 +592,6 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
620
592
|
logConversation("assistant", finalContent, sourceLabel, sessionKey);
|
|
621
593
|
}
|
|
622
594
|
catch { /* best-effort */ }
|
|
623
|
-
// Episodic memory: if enough turns have accumulated since the last
|
|
624
|
-
// summary, kick off a background write. Fire-and-forget — never blocks
|
|
625
|
-
// the user reply path.
|
|
626
595
|
if (copilotClient) {
|
|
627
596
|
maybeWriteEpisode(copilotClient).catch((err) => {
|
|
628
597
|
log.error({ err: err instanceof Error ? err.message : err }, "Episode write failed (non-fatal)");
|
|
@@ -632,7 +601,6 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
632
601
|
}
|
|
633
602
|
catch (err) {
|
|
634
603
|
const msg = err instanceof Error ? err.message : String(err);
|
|
635
|
-
// Don't retry cancelled messages
|
|
636
604
|
if (/cancelled|abort/i.test(msg)) {
|
|
637
605
|
return;
|
|
638
606
|
}
|
|
@@ -640,7 +608,6 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
640
608
|
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
641
609
|
log.warn({ msg, attempt: attempt + 1, maxRetries: MAX_RETRIES, delayMs: delay }, "Recoverable error, retrying");
|
|
642
610
|
await sleep(delay);
|
|
643
|
-
// Reset client before retry in case the connection is stale
|
|
644
611
|
try {
|
|
645
612
|
await ensureClient();
|
|
646
613
|
}
|
|
@@ -654,34 +621,28 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
654
621
|
}
|
|
655
622
|
})();
|
|
656
623
|
}
|
|
657
|
-
/** Cancel
|
|
624
|
+
/** Cancel all queued and in-flight messages across all active sessions. */
|
|
658
625
|
export async function cancelCurrentMessage() {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (activeSession && currentCallback) {
|
|
668
|
-
try {
|
|
669
|
-
await activeSession.abort();
|
|
670
|
-
log.info({ sessionKey: currentProcessingSessionKey }, "Aborted in-flight request");
|
|
671
|
-
return true;
|
|
672
|
-
}
|
|
673
|
-
catch (err) {
|
|
674
|
-
log.error({ err: err instanceof Error ? err.message : err }, "Abort failed");
|
|
626
|
+
if (!registry)
|
|
627
|
+
return false;
|
|
628
|
+
let drained = 0;
|
|
629
|
+
const aborts = [];
|
|
630
|
+
for (const [, manager] of registry.getAll()) {
|
|
631
|
+
drained += manager.cancelQueued();
|
|
632
|
+
if (manager.isProcessing) {
|
|
633
|
+
aborts.push(manager.abortCurrentTurn());
|
|
675
634
|
}
|
|
676
635
|
}
|
|
677
|
-
|
|
636
|
+
const results = await Promise.all(aborts);
|
|
637
|
+
const aborted = results.some(Boolean);
|
|
638
|
+
return aborted || drained > 0;
|
|
678
639
|
}
|
|
679
640
|
/** Switch the model on the live default orchestrator session without destroying it. */
|
|
680
641
|
export function switchSessionModel(newModel) {
|
|
681
|
-
const
|
|
682
|
-
if (session) {
|
|
683
|
-
return session.setModel(newModel).then(() => {
|
|
684
|
-
|
|
642
|
+
const manager = registry?.get("default");
|
|
643
|
+
if (manager?.session) {
|
|
644
|
+
return manager.session.setModel(newModel).then(() => {
|
|
645
|
+
manager.currentModel = newModel;
|
|
685
646
|
});
|
|
686
647
|
}
|
|
687
648
|
return Promise.resolve();
|
|
@@ -689,9 +650,9 @@ export function switchSessionModel(newModel) {
|
|
|
689
650
|
/** Return a snapshot of currently running workers for API/UI consumers. */
|
|
690
651
|
export function getAgentInfo() {
|
|
691
652
|
const allTasks = getActiveTasks().filter((t) => t.status === "running");
|
|
692
|
-
const
|
|
653
|
+
const reg = getAgentRegistry();
|
|
693
654
|
return allTasks.map((t) => {
|
|
694
|
-
const agent =
|
|
655
|
+
const agent = reg.find((a) => a.slug === t.agentSlug);
|
|
695
656
|
return {
|
|
696
657
|
slug: t.agentSlug,
|
|
697
658
|
name: agent?.name || t.agentSlug,
|
|
@@ -703,16 +664,11 @@ export function getAgentInfo() {
|
|
|
703
664
|
}
|
|
704
665
|
/** Clean up on shutdown/restart. */
|
|
705
666
|
export async function shutdownAgents() {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
catch (err) {
|
|
711
|
-
log.error({ sessionKey: key, err: err instanceof Error ? err.message : err }, "Error disconnecting session during shutdown");
|
|
712
|
-
}
|
|
667
|
+
if (!registry) {
|
|
668
|
+
await clearActiveTasks();
|
|
669
|
+
return;
|
|
713
670
|
}
|
|
714
|
-
|
|
715
|
-
sessionModelMap.clear();
|
|
671
|
+
await registry.shutdown();
|
|
716
672
|
await clearActiveTasks();
|
|
717
673
|
}
|
|
718
674
|
//# sourceMappingURL=orchestrator.js.map
|