chainlesschain 0.45.12 → 0.45.20
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AppLayout-BfLjLMsm.js → AppLayout-B00RARl2.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DP7PO9Li.js → Chat-DXtvKoM0.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DyQF-7R1.js → Cron-BJ4ODHOy.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Logs-BOii-AoO.js → Logs-CSeKZEG_.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DmiJtJYr.js → McpTools-BYQAK11r.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-CDRMMobU.js → Memory-gkUAPyuZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVhqqoS1.js → Notes-bjNrQgAo.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Dkt7021l.js → Providers-Dbf57Tbv.js} +1 -1
- package/src/assets/web-panel/assets/{Services-DUDL_UGb.js → Services-CS0oMdxh.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DXXELJc3.js → Skills-B2fgruv8.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
- package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
- package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
- package/src/assets/web-panel/assets/{index-vW799KpE.js → index-CF2CqPYX.js} +2 -2
- package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +7 -8
- package/src/commands/chat.js +9 -11
- package/src/commands/serve.js +11 -106
- package/src/commands/session.js +101 -0
- package/src/commands/ui.js +10 -151
- package/src/gateways/repl/agent-repl.js +1 -0
- package/src/gateways/repl/chat-repl.js +1 -0
- package/src/gateways/ui/web-ui-server.js +1 -0
- package/src/gateways/ws/action-protocol.js +83 -0
- package/src/gateways/ws/message-dispatcher.js +73 -0
- package/src/gateways/ws/session-protocol.js +396 -0
- package/src/gateways/ws/task-protocol.js +55 -0
- package/src/gateways/ws/worktree-protocol.js +315 -0
- package/src/gateways/ws/ws-server.js +4 -0
- package/src/gateways/ws/ws-session-gateway.js +1 -0
- package/src/harness/background-task-manager.js +506 -0
- package/src/harness/background-task-worker.js +48 -0
- package/src/harness/compression-telemetry.js +214 -0
- package/src/harness/feature-flags.js +157 -0
- package/src/harness/jsonl-session-store.js +452 -0
- package/src/harness/prompt-compressor.js +416 -0
- package/src/harness/worktree-isolator.js +845 -0
- package/src/lib/agent-core.js +246 -45
- package/src/lib/background-task-manager.js +1 -305
- package/src/lib/background-task-worker.js +1 -50
- package/src/lib/compression-telemetry.js +5 -0
- package/src/lib/feature-flags.js +7 -182
- package/src/lib/interaction-adapter.js +32 -6
- package/src/lib/jsonl-session-store.js +21 -237
- package/src/lib/prompt-compressor.js +10 -481
- package/src/lib/sub-agent-context.js +21 -1
- package/src/lib/worktree-isolator.js +13 -231
- package/src/lib/ws-agent-handler.js +1 -0
- package/src/lib/ws-server.js +138 -387
- package/src/lib/ws-session-manager.js +82 -1
- package/src/repl/agent-repl.js +11 -0
- package/src/runtime/agent-runtime.js +417 -0
- package/src/runtime/contracts/agent-turn.js +11 -0
- package/src/runtime/contracts/session-record.js +31 -0
- package/src/runtime/contracts/task-record.js +18 -0
- package/src/runtime/contracts/telemetry-record.js +23 -0
- package/src/runtime/contracts/worktree-record.js +14 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/policies/agent-policy.js +45 -0
- package/src/runtime/runtime-context.js +14 -0
- package/src/runtime/runtime-events.js +37 -0
- package/src/runtime/runtime-factory.js +50 -0
- package/src/tools/index.js +22 -0
- package/src/tools/legacy-agent-tools.js +171 -0
- package/src/tools/registry.js +141 -0
- package/src/tools/tool-context.js +28 -0
- package/src/tools/tool-permissions.js +28 -0
- package/src/tools/tool-telemetry.js +39 -0
- package/src/assets/web-panel/assets/Dashboard-BGGdnr6t.js +0 -3
- package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
- package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +0 -1
- package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +0 -1
- package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
- package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
package/src/lib/ws-server.js
CHANGED
|
@@ -11,6 +11,38 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { WebSocketServer } from "ws";
|
|
14
|
+
import { createTaskRecord } from "../runtime/contracts/task-record.js";
|
|
15
|
+
import {
|
|
16
|
+
RUNTIME_EVENTS,
|
|
17
|
+
createRuntimeEvent,
|
|
18
|
+
} from "../runtime/runtime-events.js";
|
|
19
|
+
import { createWsMessageDispatcher } from "../gateways/ws/message-dispatcher.js";
|
|
20
|
+
import {
|
|
21
|
+
handleTaskDetail,
|
|
22
|
+
handleTaskHistory,
|
|
23
|
+
} from "../gateways/ws/task-protocol.js";
|
|
24
|
+
import {
|
|
25
|
+
handleSessionCreate,
|
|
26
|
+
handleSessionResume,
|
|
27
|
+
handleSessionMessage,
|
|
28
|
+
handleSessionPolicyUpdate,
|
|
29
|
+
handleSessionList,
|
|
30
|
+
handleSessionClose,
|
|
31
|
+
handleSessionAnswer,
|
|
32
|
+
handleHostToolResult,
|
|
33
|
+
} from "../gateways/ws/session-protocol.js";
|
|
34
|
+
import {
|
|
35
|
+
handleSlashCommand,
|
|
36
|
+
handleOrchestrate,
|
|
37
|
+
} from "../gateways/ws/action-protocol.js";
|
|
38
|
+
import {
|
|
39
|
+
handleWorktreeDiff,
|
|
40
|
+
handleWorktreeMerge,
|
|
41
|
+
handleWorktreeMergePreview,
|
|
42
|
+
handleWorktreeAutomationApply,
|
|
43
|
+
handleWorktreeList,
|
|
44
|
+
handleCompressionStats,
|
|
45
|
+
} from "../gateways/ws/worktree-protocol.js";
|
|
14
46
|
|
|
15
47
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
48
|
const __dirname = dirname(__filename);
|
|
@@ -99,6 +131,7 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
99
131
|
|
|
100
132
|
/** Session handlers: sessionId → WSAgentHandler | WSChatHandler */
|
|
101
133
|
this.sessionHandlers = new Map();
|
|
134
|
+
this._dispatcher = createWsMessageDispatcher(this);
|
|
102
135
|
|
|
103
136
|
this._heartbeatTimer = null;
|
|
104
137
|
this._clientCounter = 0;
|
|
@@ -227,83 +260,7 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
227
260
|
|
|
228
261
|
/** @private */
|
|
229
262
|
async _handleMessage(clientId, ws, message) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!id) {
|
|
233
|
-
this._send(ws, {
|
|
234
|
-
type: "error",
|
|
235
|
-
code: "MISSING_ID",
|
|
236
|
-
message: 'Message must include an "id" field',
|
|
237
|
-
});
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Check authentication
|
|
242
|
-
const client = this.clients.get(clientId);
|
|
243
|
-
if (this.token && !client.authenticated && type !== "auth") {
|
|
244
|
-
this._send(ws, {
|
|
245
|
-
id,
|
|
246
|
-
type: "error",
|
|
247
|
-
code: "AUTH_REQUIRED",
|
|
248
|
-
message: "Authentication required. Send an auth message first.",
|
|
249
|
-
});
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
switch (type) {
|
|
254
|
-
case "auth":
|
|
255
|
-
this._handleAuth(clientId, ws, message);
|
|
256
|
-
break;
|
|
257
|
-
case "ping":
|
|
258
|
-
this._send(ws, { id, type: "pong", serverTime: Date.now() });
|
|
259
|
-
break;
|
|
260
|
-
case "execute":
|
|
261
|
-
this._executeCommand(id, ws, message.command, false);
|
|
262
|
-
break;
|
|
263
|
-
case "stream":
|
|
264
|
-
this._executeCommand(id, ws, message.command, true);
|
|
265
|
-
break;
|
|
266
|
-
case "cancel":
|
|
267
|
-
this._cancelRequest(id, ws);
|
|
268
|
-
break;
|
|
269
|
-
case "session-create":
|
|
270
|
-
await this._handleSessionCreate(id, ws, message);
|
|
271
|
-
break;
|
|
272
|
-
case "session-resume":
|
|
273
|
-
await this._handleSessionResume(id, ws, message);
|
|
274
|
-
break;
|
|
275
|
-
case "session-message":
|
|
276
|
-
this._handleSessionMessage(id, ws, message);
|
|
277
|
-
break;
|
|
278
|
-
case "session-list":
|
|
279
|
-
this._handleSessionList(id, ws);
|
|
280
|
-
break;
|
|
281
|
-
case "session-close":
|
|
282
|
-
this._handleSessionClose(id, ws, message);
|
|
283
|
-
break;
|
|
284
|
-
case "slash-command":
|
|
285
|
-
this._handleSlashCommand(id, ws, message);
|
|
286
|
-
break;
|
|
287
|
-
case "session-answer":
|
|
288
|
-
this._handleSessionAnswer(id, ws, message);
|
|
289
|
-
break;
|
|
290
|
-
case "orchestrate":
|
|
291
|
-
this._handleOrchestrate(id, ws, message);
|
|
292
|
-
break;
|
|
293
|
-
case "tasks-list":
|
|
294
|
-
this._handleTasksList(id, ws);
|
|
295
|
-
break;
|
|
296
|
-
case "tasks-stop":
|
|
297
|
-
this._handleTasksStop(id, ws, message);
|
|
298
|
-
break;
|
|
299
|
-
default:
|
|
300
|
-
this._send(ws, {
|
|
301
|
-
id,
|
|
302
|
-
type: "error",
|
|
303
|
-
code: "UNKNOWN_TYPE",
|
|
304
|
-
message: `Unknown message type: ${type}`,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
263
|
+
return this._dispatcher.dispatch(clientId, ws, message);
|
|
307
264
|
}
|
|
308
265
|
|
|
309
266
|
/**
|
|
@@ -320,84 +277,13 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
320
277
|
* { type: "error", code: "ORCHESTRATE_FAILED", ... }
|
|
321
278
|
*/
|
|
322
279
|
async _handleOrchestrate(id, ws, message) {
|
|
323
|
-
|
|
324
|
-
task,
|
|
325
|
-
cwd,
|
|
326
|
-
agents = 3,
|
|
327
|
-
ci = "npm test",
|
|
328
|
-
noCi = false,
|
|
329
|
-
strategy,
|
|
330
|
-
} = message;
|
|
331
|
-
|
|
332
|
-
if (!task || typeof task !== "string") {
|
|
333
|
-
this._send(ws, {
|
|
334
|
-
id,
|
|
335
|
-
type: "error",
|
|
336
|
-
code: "INVALID_TASK",
|
|
337
|
-
message: "task field required",
|
|
338
|
-
});
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const { Orchestrator, TASK_SOURCE } = await import("./orchestrator.js");
|
|
344
|
-
|
|
345
|
-
const orch = new Orchestrator({
|
|
346
|
-
cwd: cwd || this.projectRoot || process.cwd(),
|
|
347
|
-
maxParallel: Math.min(parseInt(agents, 10) || 3, 10),
|
|
348
|
-
ciCommand: ci,
|
|
349
|
-
agents: strategy ? { strategy } : undefined,
|
|
350
|
-
verbose: false,
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// Add WebSocket as a notification channel — all events go back to this client
|
|
354
|
-
const wsNotifier = orch.notifier.addWebSocketChannel({
|
|
355
|
-
send: (data) => this._send(ws, data),
|
|
356
|
-
requestId: id,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Forward real-time agent output
|
|
360
|
-
orch.on("agent:output", (ev) => wsNotifier.sendAgentOutput(ev));
|
|
361
|
-
|
|
362
|
-
// Forward task status changes
|
|
363
|
-
orch.on("task:added", (t) => wsNotifier.sendStatus(t));
|
|
364
|
-
orch.on("task:decomposing", (t) => wsNotifier.sendStatus(t));
|
|
365
|
-
orch.on("ci:checking", ({ task: t }) => wsNotifier.sendStatus(t));
|
|
366
|
-
orch.on("task:retrying", ({ task: t }) => wsNotifier.sendStatus(t));
|
|
367
|
-
|
|
368
|
-
const result = await orch.addTask(task, {
|
|
369
|
-
source: TASK_SOURCE.CLI,
|
|
370
|
-
cwd: cwd || this.projectRoot || process.cwd(),
|
|
371
|
-
runCI: !noCi,
|
|
372
|
-
notify: true,
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
this._send(ws, {
|
|
376
|
-
id,
|
|
377
|
-
type: "orchestrate:done",
|
|
378
|
-
taskId: result.id,
|
|
379
|
-
status: result.status,
|
|
380
|
-
retries: result.retries,
|
|
381
|
-
subtasks: result.subtasks?.length || 0,
|
|
382
|
-
});
|
|
383
|
-
} catch (err) {
|
|
384
|
-
this._send(ws, {
|
|
385
|
-
id,
|
|
386
|
-
type: "error",
|
|
387
|
-
code: "ORCHESTRATE_FAILED",
|
|
388
|
-
message: err.message,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
280
|
+
return handleOrchestrate(this, id, ws, message);
|
|
391
281
|
}
|
|
392
282
|
|
|
393
|
-
/** @private
|
|
394
|
-
_handleTasksList(id, ws) {
|
|
283
|
+
/** @private – list background tasks */
|
|
284
|
+
async _handleTasksList(id, ws) {
|
|
395
285
|
try {
|
|
396
|
-
|
|
397
|
-
// Reuse singleton or create lightweight instance for listing
|
|
398
|
-
if (!this._taskManager) {
|
|
399
|
-
this._taskManager = new BackgroundTaskManager();
|
|
400
|
-
}
|
|
286
|
+
await this._ensureTaskManager();
|
|
401
287
|
const tasks = this._taskManager.list();
|
|
402
288
|
this._send(ws, { id, type: "tasks-list", tasks });
|
|
403
289
|
} catch (err) {
|
|
@@ -405,9 +291,35 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
405
291
|
}
|
|
406
292
|
}
|
|
407
293
|
|
|
408
|
-
/** @private —
|
|
409
|
-
|
|
294
|
+
/** @private — subscribe to task completion events and broadcast to all clients */
|
|
295
|
+
_subscribeTaskNotifications() {
|
|
296
|
+
if (!this._taskManager || this._taskNotificationsSubscribed) return;
|
|
297
|
+
this._taskNotificationsSubscribed = true;
|
|
298
|
+
|
|
299
|
+
this._taskManager.on("task:complete", (task) => {
|
|
300
|
+
const record = createTaskRecord(task, {
|
|
301
|
+
source: "background-task-manager",
|
|
302
|
+
});
|
|
303
|
+
this.emit(
|
|
304
|
+
RUNTIME_EVENTS.TASK_NOTIFICATION,
|
|
305
|
+
createRuntimeEvent(
|
|
306
|
+
RUNTIME_EVENTS.TASK_NOTIFICATION,
|
|
307
|
+
{ task: record },
|
|
308
|
+
{ kind: "server" },
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
this._broadcast({
|
|
312
|
+
type: "task:notification",
|
|
313
|
+
task: record,
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @private – stop a background task */
|
|
319
|
+
async _handleTasksStop(id, ws, message) {
|
|
410
320
|
try {
|
|
321
|
+
await this._ensureTaskManager();
|
|
322
|
+
|
|
411
323
|
if (this._taskManager && message.taskId) {
|
|
412
324
|
this._taskManager.stop(message.taskId);
|
|
413
325
|
this._send(ws, { id, type: "tasks-stopped", taskId: message.taskId });
|
|
@@ -429,6 +341,46 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
429
341
|
}
|
|
430
342
|
}
|
|
431
343
|
|
|
344
|
+
/** @private */
|
|
345
|
+
async _handleTaskDetail(id, ws, message) {
|
|
346
|
+
return handleTaskDetail(this, id, ws, message);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** @private */
|
|
350
|
+
async _handleTaskHistory(id, ws, message) {
|
|
351
|
+
return handleTaskHistory(this, id, ws, message);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @private — diff preview for agent worktree branch */
|
|
355
|
+
async _handleWorktreeDiff(id, ws, message) {
|
|
356
|
+
return handleWorktreeDiff(this, id, ws, message);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @private — one-click merge of agent worktree branch */
|
|
360
|
+
async _handleWorktreeMerge(id, ws, message) {
|
|
361
|
+
return handleWorktreeMerge(this, id, ws, message);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** @private - dry-run merge preview for an agent worktree branch */
|
|
365
|
+
async _handleWorktreeMergePreview(id, ws, message) {
|
|
366
|
+
return handleWorktreeMergePreview(this, id, ws, message);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @private - apply a safe automation candidate inside an agent worktree */
|
|
370
|
+
async _handleWorktreeAutomationApply(id, ws, message) {
|
|
371
|
+
return handleWorktreeAutomationApply(this, id, ws, message);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** @private - list agent worktrees */
|
|
375
|
+
async _handleWorktreeList(id, ws) {
|
|
376
|
+
return handleWorktreeList(this, id, ws);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @private */
|
|
380
|
+
async _handleCompressionStats(id, ws, message) {
|
|
381
|
+
return handleCompressionStats(this, id, ws, message);
|
|
382
|
+
}
|
|
383
|
+
|
|
432
384
|
/** @private */
|
|
433
385
|
_handleAuth(clientId, ws, message) {
|
|
434
386
|
const { id, token } = message;
|
|
@@ -619,268 +571,58 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
619
571
|
|
|
620
572
|
/** @private */
|
|
621
573
|
async _handleSessionCreate(id, ws, message) {
|
|
622
|
-
|
|
623
|
-
this._send(ws, {
|
|
624
|
-
id,
|
|
625
|
-
type: "error",
|
|
626
|
-
code: "NO_SESSION_SUPPORT",
|
|
627
|
-
message: "Session support not configured on this server",
|
|
628
|
-
});
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const { sessionType, provider, model, apiKey, baseUrl, projectRoot } =
|
|
633
|
-
message;
|
|
634
|
-
|
|
635
|
-
try {
|
|
636
|
-
const { sessionId } = this.sessionManager.createSession({
|
|
637
|
-
type: sessionType || "agent",
|
|
638
|
-
provider,
|
|
639
|
-
model,
|
|
640
|
-
apiKey,
|
|
641
|
-
baseUrl,
|
|
642
|
-
projectRoot,
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
const session = this.sessionManager.getSession(sessionId);
|
|
646
|
-
|
|
647
|
-
// Lazy-load handler modules to avoid circular deps
|
|
648
|
-
try {
|
|
649
|
-
const { WebSocketInteractionAdapter } =
|
|
650
|
-
await import("./interaction-adapter.js");
|
|
651
|
-
session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
|
|
652
|
-
|
|
653
|
-
let handler;
|
|
654
|
-
if ((sessionType || "agent") === "chat") {
|
|
655
|
-
const { WSChatHandler } = await import("./ws-chat-handler.js");
|
|
656
|
-
handler = new WSChatHandler({
|
|
657
|
-
session,
|
|
658
|
-
interaction: session.interaction,
|
|
659
|
-
});
|
|
660
|
-
} else {
|
|
661
|
-
const { WSAgentHandler } = await import("./ws-agent-handler.js");
|
|
662
|
-
handler = new WSAgentHandler({
|
|
663
|
-
session,
|
|
664
|
-
interaction: session.interaction,
|
|
665
|
-
db: this.sessionManager.db,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
this.sessionHandlers.set(sessionId, handler);
|
|
669
|
-
} catch (_err) {
|
|
670
|
-
// Handler creation failed — session still created, handler can be set later
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
this.emit("session:create", { sessionId, type: sessionType || "agent" });
|
|
674
|
-
|
|
675
|
-
this._send(ws, {
|
|
676
|
-
id,
|
|
677
|
-
type: "session-created",
|
|
678
|
-
sessionId,
|
|
679
|
-
sessionType: sessionType || "agent",
|
|
680
|
-
});
|
|
681
|
-
} catch (err) {
|
|
682
|
-
this._send(ws, {
|
|
683
|
-
id,
|
|
684
|
-
type: "error",
|
|
685
|
-
code: "SESSION_CREATE_FAILED",
|
|
686
|
-
message: err.message,
|
|
687
|
-
});
|
|
688
|
-
}
|
|
574
|
+
return handleSessionCreate(this, id, ws, message);
|
|
689
575
|
}
|
|
690
576
|
|
|
691
577
|
/** @private */
|
|
692
578
|
async _handleSessionResume(id, ws, message) {
|
|
693
|
-
|
|
694
|
-
this._send(ws, {
|
|
695
|
-
id,
|
|
696
|
-
type: "error",
|
|
697
|
-
code: "NO_SESSION_SUPPORT",
|
|
698
|
-
message: "Session support not configured",
|
|
699
|
-
});
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const { sessionId } = message;
|
|
704
|
-
const session = this.sessionManager.resumeSession(sessionId);
|
|
705
|
-
|
|
706
|
-
if (!session) {
|
|
707
|
-
this._send(ws, {
|
|
708
|
-
id,
|
|
709
|
-
type: "error",
|
|
710
|
-
code: "SESSION_NOT_FOUND",
|
|
711
|
-
message: `Session not found: ${sessionId}`,
|
|
712
|
-
});
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Rebuild interaction adapter and handler for the resumed session
|
|
717
|
-
if (!this.sessionHandlers.has(sessionId)) {
|
|
718
|
-
try {
|
|
719
|
-
const { WebSocketInteractionAdapter } =
|
|
720
|
-
await import("./interaction-adapter.js");
|
|
721
|
-
session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
|
|
722
|
-
|
|
723
|
-
let handler;
|
|
724
|
-
if (session.type === "chat") {
|
|
725
|
-
const { WSChatHandler } = await import("./ws-chat-handler.js");
|
|
726
|
-
handler = new WSChatHandler({
|
|
727
|
-
session,
|
|
728
|
-
interaction: session.interaction,
|
|
729
|
-
});
|
|
730
|
-
} else {
|
|
731
|
-
const { WSAgentHandler } = await import("./ws-agent-handler.js");
|
|
732
|
-
handler = new WSAgentHandler({
|
|
733
|
-
session,
|
|
734
|
-
interaction: session.interaction,
|
|
735
|
-
db: this.sessionManager.db,
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
this.sessionHandlers.set(sessionId, handler);
|
|
739
|
-
} catch (_err) {
|
|
740
|
-
// Handler creation failed — session resumed without handler
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Filter out system messages for history
|
|
745
|
-
const history = (session.messages || []).filter((m) => m.role !== "system");
|
|
746
|
-
|
|
747
|
-
this._send(ws, {
|
|
748
|
-
id,
|
|
749
|
-
type: "session-resumed",
|
|
750
|
-
sessionId: session.id,
|
|
751
|
-
history,
|
|
752
|
-
});
|
|
579
|
+
return handleSessionResume(this, id, ws, message);
|
|
753
580
|
}
|
|
754
581
|
|
|
755
582
|
/** @private */
|
|
756
583
|
_handleSessionMessage(id, ws, message) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (!handler) {
|
|
761
|
-
this._send(ws, {
|
|
762
|
-
id,
|
|
763
|
-
type: "error",
|
|
764
|
-
code: "SESSION_NOT_FOUND",
|
|
765
|
-
message: `No active session handler for: ${sessionId}`,
|
|
766
|
-
});
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
584
|
+
return handleSessionMessage(this, id, ws, message);
|
|
585
|
+
}
|
|
769
586
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
.then(() => {
|
|
774
|
-
// Persist messages after each turn
|
|
775
|
-
if (this.sessionManager) {
|
|
776
|
-
try {
|
|
777
|
-
this.sessionManager.persistMessages(sessionId);
|
|
778
|
-
} catch (_err) {
|
|
779
|
-
// Non-critical
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
})
|
|
783
|
-
.catch((err) => {
|
|
784
|
-
this._send(ws, {
|
|
785
|
-
id,
|
|
786
|
-
type: "error",
|
|
787
|
-
code: "MESSAGE_FAILED",
|
|
788
|
-
message: err.message,
|
|
789
|
-
});
|
|
790
|
-
});
|
|
587
|
+
/** @private */
|
|
588
|
+
_handleSessionPolicyUpdate(id, ws, message) {
|
|
589
|
+
return handleSessionPolicyUpdate(this, id, ws, message);
|
|
791
590
|
}
|
|
792
591
|
|
|
793
592
|
/** @private */
|
|
794
593
|
_handleSessionList(id, ws) {
|
|
795
|
-
|
|
796
|
-
this._send(ws, {
|
|
797
|
-
id,
|
|
798
|
-
type: "error",
|
|
799
|
-
code: "NO_SESSION_SUPPORT",
|
|
800
|
-
message: "Session support not configured",
|
|
801
|
-
});
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const sessions = this.sessionManager.listSessions();
|
|
806
|
-
this._send(ws, {
|
|
807
|
-
id,
|
|
808
|
-
type: "session-list-result",
|
|
809
|
-
sessions,
|
|
810
|
-
});
|
|
594
|
+
return handleSessionList(this, id, ws);
|
|
811
595
|
}
|
|
812
596
|
|
|
813
597
|
/** @private */
|
|
814
598
|
_handleSessionClose(id, ws, message) {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
// Remove handler
|
|
818
|
-
const handler = this.sessionHandlers.get(sessionId);
|
|
819
|
-
if (handler && handler.destroy) {
|
|
820
|
-
handler.destroy();
|
|
821
|
-
}
|
|
822
|
-
this.sessionHandlers.delete(sessionId);
|
|
823
|
-
|
|
824
|
-
// Close session in manager
|
|
825
|
-
if (this.sessionManager) {
|
|
826
|
-
try {
|
|
827
|
-
this.sessionManager.closeSession(sessionId);
|
|
828
|
-
} catch (_err) {
|
|
829
|
-
// Non-critical
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
this.emit("session:close", { sessionId });
|
|
834
|
-
|
|
835
|
-
this._send(ws, {
|
|
836
|
-
id,
|
|
837
|
-
type: "result",
|
|
838
|
-
success: true,
|
|
839
|
-
sessionId,
|
|
840
|
-
});
|
|
599
|
+
return handleSessionClose(this, id, ws, message);
|
|
841
600
|
}
|
|
842
601
|
|
|
843
602
|
/** @private */
|
|
844
603
|
_handleSlashCommand(id, ws, message) {
|
|
845
|
-
|
|
846
|
-
const handler = this.sessionHandlers.get(sessionId);
|
|
847
|
-
|
|
848
|
-
if (!handler) {
|
|
849
|
-
this._send(ws, {
|
|
850
|
-
id,
|
|
851
|
-
type: "error",
|
|
852
|
-
code: "SESSION_NOT_FOUND",
|
|
853
|
-
message: `No active session handler for: ${sessionId}`,
|
|
854
|
-
});
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
handler.handleSlashCommand(command, id);
|
|
604
|
+
return handleSlashCommand(this, id, ws, message);
|
|
859
605
|
}
|
|
860
606
|
|
|
861
607
|
/** @private */
|
|
862
608
|
_handleSessionAnswer(id, ws, message) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (!this.sessionManager) {
|
|
866
|
-
this._send(ws, {
|
|
867
|
-
id,
|
|
868
|
-
type: "error",
|
|
869
|
-
code: "NO_SESSION_SUPPORT",
|
|
870
|
-
message: "Session support not configured",
|
|
871
|
-
});
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const session = this.sessionManager.getSession(sessionId);
|
|
876
|
-
if (session && session.interaction && session.interaction.resolveAnswer) {
|
|
877
|
-
session.interaction.resolveAnswer(requestId, answer);
|
|
878
|
-
}
|
|
609
|
+
return handleSessionAnswer(this, id, ws, message);
|
|
610
|
+
}
|
|
879
611
|
|
|
880
|
-
|
|
612
|
+
_handleHostToolResult(id, ws, message) {
|
|
613
|
+
return handleHostToolResult(this, id, ws, message);
|
|
881
614
|
}
|
|
882
615
|
|
|
883
616
|
/** @private — ping/pong heartbeat to detect dead connections */
|
|
617
|
+
async _ensureTaskManager() {
|
|
618
|
+
if (this._taskManager) return this._taskManager;
|
|
619
|
+
const { BackgroundTaskManager } =
|
|
620
|
+
await import("./background-task-manager.js");
|
|
621
|
+
this._taskManager = new BackgroundTaskManager({ recoverOnStart: true });
|
|
622
|
+
this._subscribeTaskNotifications();
|
|
623
|
+
return this._taskManager;
|
|
624
|
+
}
|
|
625
|
+
|
|
884
626
|
_startHeartbeat() {
|
|
885
627
|
this._heartbeatTimer = setInterval(() => {
|
|
886
628
|
for (const [clientId, client] of this.clients) {
|
|
@@ -910,4 +652,13 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
910
652
|
}
|
|
911
653
|
}
|
|
912
654
|
}
|
|
655
|
+
|
|
656
|
+
/** @private — broadcast a message to all connected, authenticated clients */
|
|
657
|
+
_broadcast(data) {
|
|
658
|
+
for (const [, client] of this.clients) {
|
|
659
|
+
if (client.authenticated || !this.token) {
|
|
660
|
+
this._send(client.ws, data);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
913
664
|
}
|