chainlesschain 0.45.67 → 0.45.70
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
CHANGED
|
@@ -66,6 +66,15 @@ export function createWsMessageDispatcher(server) {
|
|
|
66
66
|
"patch-apply": () => server._handlePatchApply(id, ws, message),
|
|
67
67
|
"patch-reject": () => server._handlePatchReject(id, ws, message),
|
|
68
68
|
"patch-summary": () => server._handlePatchSummary(id, ws, message),
|
|
69
|
+
"task-graph-create": () =>
|
|
70
|
+
server._handleTaskGraphCreate(id, ws, message),
|
|
71
|
+
"task-graph-add-node": () =>
|
|
72
|
+
server._handleTaskGraphAddNode(id, ws, message),
|
|
73
|
+
"task-graph-update-node": () =>
|
|
74
|
+
server._handleTaskGraphUpdateNode(id, ws, message),
|
|
75
|
+
"task-graph-advance": () =>
|
|
76
|
+
server._handleTaskGraphAdvance(id, ws, message),
|
|
77
|
+
"task-graph-state": () => server._handleTaskGraphState(id, ws, message),
|
|
69
78
|
};
|
|
70
79
|
|
|
71
80
|
const handler = routes[type];
|
|
@@ -1164,6 +1164,373 @@ export function handlePatchSummary(server, id, ws, message) {
|
|
|
1164
1164
|
);
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
+
/**
|
|
1168
|
+
* Helper: emit a task-graph.* envelope through the session's interaction
|
|
1169
|
+
* adapter (same fan-out pattern as _emitPatchEvent).
|
|
1170
|
+
*/
|
|
1171
|
+
function _emitTaskGraphEvent(server, session, type, payload, ws) {
|
|
1172
|
+
const envelope = createCodingAgentEvent(
|
|
1173
|
+
type,
|
|
1174
|
+
{ ...(payload || {}), sessionId: session.id },
|
|
1175
|
+
{
|
|
1176
|
+
sessionId: session.id,
|
|
1177
|
+
source: "cli-runtime",
|
|
1178
|
+
},
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
const interaction = session && session.interaction;
|
|
1182
|
+
if (interaction && typeof interaction.emit === "function") {
|
|
1183
|
+
try {
|
|
1184
|
+
interaction.emit(type, envelope.payload);
|
|
1185
|
+
return;
|
|
1186
|
+
} catch (_err) {
|
|
1187
|
+
// Fall through to ws send below.
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (ws) {
|
|
1192
|
+
server._send(ws, envelope);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Create a session-scoped task graph.
|
|
1198
|
+
*
|
|
1199
|
+
* Message shape:
|
|
1200
|
+
* { type: "task-graph-create", id, sessionId, title?, nodes: [...] }
|
|
1201
|
+
*/
|
|
1202
|
+
export function handleTaskGraphCreate(server, id, ws, message) {
|
|
1203
|
+
const { sessionId } = message || {};
|
|
1204
|
+
|
|
1205
|
+
if (!server.sessionManager) {
|
|
1206
|
+
server._send(
|
|
1207
|
+
ws,
|
|
1208
|
+
envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
|
|
1209
|
+
);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const session = server.sessionManager.getSession(sessionId);
|
|
1214
|
+
if (!session) {
|
|
1215
|
+
server._send(
|
|
1216
|
+
ws,
|
|
1217
|
+
envelopeError(
|
|
1218
|
+
id,
|
|
1219
|
+
"SESSION_NOT_FOUND",
|
|
1220
|
+
`Session not found: ${sessionId}`,
|
|
1221
|
+
sessionId,
|
|
1222
|
+
),
|
|
1223
|
+
);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (!Array.isArray(message.nodes)) {
|
|
1228
|
+
server._send(
|
|
1229
|
+
ws,
|
|
1230
|
+
envelopeError(
|
|
1231
|
+
id,
|
|
1232
|
+
"INVALID_PAYLOAD",
|
|
1233
|
+
"task-graph-create requires a nodes array",
|
|
1234
|
+
sessionId,
|
|
1235
|
+
),
|
|
1236
|
+
);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const graph = server.sessionManager.createTaskGraph(sessionId, {
|
|
1241
|
+
graphId: message.graphId,
|
|
1242
|
+
title: message.title,
|
|
1243
|
+
description: message.description,
|
|
1244
|
+
nodes: message.nodes,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
server._send(
|
|
1248
|
+
ws,
|
|
1249
|
+
envelopeResponse(
|
|
1250
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_CREATED,
|
|
1251
|
+
id,
|
|
1252
|
+
{ sessionId, graph },
|
|
1253
|
+
sessionId,
|
|
1254
|
+
),
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
_emitTaskGraphEvent(
|
|
1258
|
+
server,
|
|
1259
|
+
session,
|
|
1260
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_CREATED,
|
|
1261
|
+
{ graph },
|
|
1262
|
+
ws,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Add a node to an existing task graph.
|
|
1268
|
+
*
|
|
1269
|
+
* Message shape:
|
|
1270
|
+
* { type: "task-graph-add-node", id, sessionId, node: { id, title, dependsOn? } }
|
|
1271
|
+
*/
|
|
1272
|
+
export function handleTaskGraphAddNode(server, id, ws, message) {
|
|
1273
|
+
const { sessionId } = message || {};
|
|
1274
|
+
|
|
1275
|
+
if (!server.sessionManager) {
|
|
1276
|
+
server._send(
|
|
1277
|
+
ws,
|
|
1278
|
+
envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
|
|
1279
|
+
);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const session = server.sessionManager.getSession(sessionId);
|
|
1284
|
+
if (!session) {
|
|
1285
|
+
server._send(
|
|
1286
|
+
ws,
|
|
1287
|
+
envelopeError(
|
|
1288
|
+
id,
|
|
1289
|
+
"SESSION_NOT_FOUND",
|
|
1290
|
+
`Session not found: ${sessionId}`,
|
|
1291
|
+
sessionId,
|
|
1292
|
+
),
|
|
1293
|
+
);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const node = message.node || null;
|
|
1298
|
+
if (!node || !node.id) {
|
|
1299
|
+
server._send(
|
|
1300
|
+
ws,
|
|
1301
|
+
envelopeError(
|
|
1302
|
+
id,
|
|
1303
|
+
"INVALID_PAYLOAD",
|
|
1304
|
+
"task-graph-add-node requires node.id",
|
|
1305
|
+
sessionId,
|
|
1306
|
+
),
|
|
1307
|
+
);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const graph = server.sessionManager.addTaskNode(sessionId, node);
|
|
1312
|
+
if (!graph) {
|
|
1313
|
+
server._send(
|
|
1314
|
+
ws,
|
|
1315
|
+
envelopeError(
|
|
1316
|
+
id,
|
|
1317
|
+
"TASK_GRAPH_ADD_FAILED",
|
|
1318
|
+
"Unable to add node (no graph, or duplicate id)",
|
|
1319
|
+
sessionId,
|
|
1320
|
+
),
|
|
1321
|
+
);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
server._send(
|
|
1326
|
+
ws,
|
|
1327
|
+
envelopeResponse(
|
|
1328
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_ADDED,
|
|
1329
|
+
id,
|
|
1330
|
+
{ sessionId, graph, nodeId: node.id },
|
|
1331
|
+
sessionId,
|
|
1332
|
+
),
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
_emitTaskGraphEvent(
|
|
1336
|
+
server,
|
|
1337
|
+
session,
|
|
1338
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_ADDED,
|
|
1339
|
+
{ graph, nodeId: node.id },
|
|
1340
|
+
ws,
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Update a task graph node (status, result, error, metadata).
|
|
1346
|
+
*
|
|
1347
|
+
* Message shape:
|
|
1348
|
+
* { type: "task-graph-update-node", id, sessionId, nodeId, updates: { status?, result?, error? } }
|
|
1349
|
+
*/
|
|
1350
|
+
export function handleTaskGraphUpdateNode(server, id, ws, message) {
|
|
1351
|
+
const { sessionId, nodeId } = message || {};
|
|
1352
|
+
|
|
1353
|
+
if (!server.sessionManager) {
|
|
1354
|
+
server._send(
|
|
1355
|
+
ws,
|
|
1356
|
+
envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
|
|
1357
|
+
);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const session = server.sessionManager.getSession(sessionId);
|
|
1362
|
+
if (!session) {
|
|
1363
|
+
server._send(
|
|
1364
|
+
ws,
|
|
1365
|
+
envelopeError(
|
|
1366
|
+
id,
|
|
1367
|
+
"SESSION_NOT_FOUND",
|
|
1368
|
+
`Session not found: ${sessionId}`,
|
|
1369
|
+
sessionId,
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (!nodeId) {
|
|
1376
|
+
server._send(
|
|
1377
|
+
ws,
|
|
1378
|
+
envelopeError(id, "INVALID_PAYLOAD", "nodeId is required", sessionId),
|
|
1379
|
+
);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const graph = server.sessionManager.updateTaskNode(
|
|
1384
|
+
sessionId,
|
|
1385
|
+
nodeId,
|
|
1386
|
+
message.updates || {},
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
if (!graph) {
|
|
1390
|
+
server._send(
|
|
1391
|
+
ws,
|
|
1392
|
+
envelopeError(
|
|
1393
|
+
id,
|
|
1394
|
+
"TASK_GRAPH_NODE_NOT_FOUND",
|
|
1395
|
+
`Task node not found: ${nodeId}`,
|
|
1396
|
+
sessionId,
|
|
1397
|
+
),
|
|
1398
|
+
);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const node = graph.nodes[nodeId];
|
|
1403
|
+
let eventType = CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_UPDATED;
|
|
1404
|
+
if (node && node.status === "completed") {
|
|
1405
|
+
eventType = CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_COMPLETED;
|
|
1406
|
+
} else if (node && node.status === "failed") {
|
|
1407
|
+
eventType = CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_FAILED;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
server._send(
|
|
1411
|
+
ws,
|
|
1412
|
+
envelopeResponse(eventType, id, { sessionId, graph, nodeId }, sessionId),
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
_emitTaskGraphEvent(server, session, eventType, { graph, nodeId }, ws);
|
|
1416
|
+
|
|
1417
|
+
if (graph.status === "completed" || graph.status === "failed") {
|
|
1418
|
+
_emitTaskGraphEvent(
|
|
1419
|
+
server,
|
|
1420
|
+
session,
|
|
1421
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_COMPLETED,
|
|
1422
|
+
{ graph },
|
|
1423
|
+
ws,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Advance the task graph: promote any pending node whose deps are satisfied.
|
|
1430
|
+
*
|
|
1431
|
+
* Message shape: { type: "task-graph-advance", id, sessionId }
|
|
1432
|
+
*/
|
|
1433
|
+
export function handleTaskGraphAdvance(server, id, ws, message) {
|
|
1434
|
+
const { sessionId } = message || {};
|
|
1435
|
+
|
|
1436
|
+
if (!server.sessionManager) {
|
|
1437
|
+
server._send(
|
|
1438
|
+
ws,
|
|
1439
|
+
envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
|
|
1440
|
+
);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const session = server.sessionManager.getSession(sessionId);
|
|
1445
|
+
if (!session) {
|
|
1446
|
+
server._send(
|
|
1447
|
+
ws,
|
|
1448
|
+
envelopeError(
|
|
1449
|
+
id,
|
|
1450
|
+
"SESSION_NOT_FOUND",
|
|
1451
|
+
`Session not found: ${sessionId}`,
|
|
1452
|
+
sessionId,
|
|
1453
|
+
),
|
|
1454
|
+
);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const result = server.sessionManager.advanceTaskGraph(sessionId);
|
|
1459
|
+
if (!result) {
|
|
1460
|
+
server._send(
|
|
1461
|
+
ws,
|
|
1462
|
+
envelopeError(
|
|
1463
|
+
id,
|
|
1464
|
+
"TASK_GRAPH_NOT_FOUND",
|
|
1465
|
+
"No task graph on session",
|
|
1466
|
+
sessionId,
|
|
1467
|
+
),
|
|
1468
|
+
);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
server._send(
|
|
1473
|
+
ws,
|
|
1474
|
+
envelopeResponse(
|
|
1475
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_ADVANCED,
|
|
1476
|
+
id,
|
|
1477
|
+
{ sessionId, graph: result.graph, becameReady: result.becameReady },
|
|
1478
|
+
sessionId,
|
|
1479
|
+
),
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
_emitTaskGraphEvent(
|
|
1483
|
+
server,
|
|
1484
|
+
session,
|
|
1485
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_ADVANCED,
|
|
1486
|
+
{ graph: result.graph, becameReady: result.becameReady },
|
|
1487
|
+
ws,
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Fetch the current task graph state.
|
|
1493
|
+
*
|
|
1494
|
+
* Message shape: { type: "task-graph-state", id, sessionId }
|
|
1495
|
+
*/
|
|
1496
|
+
export function handleTaskGraphState(server, id, ws, message) {
|
|
1497
|
+
const { sessionId } = message || {};
|
|
1498
|
+
|
|
1499
|
+
if (!server.sessionManager) {
|
|
1500
|
+
server._send(
|
|
1501
|
+
ws,
|
|
1502
|
+
envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
|
|
1503
|
+
);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const session = server.sessionManager.getSession(sessionId);
|
|
1508
|
+
if (!session) {
|
|
1509
|
+
server._send(
|
|
1510
|
+
ws,
|
|
1511
|
+
envelopeError(
|
|
1512
|
+
id,
|
|
1513
|
+
"SESSION_NOT_FOUND",
|
|
1514
|
+
`Session not found: ${sessionId}`,
|
|
1515
|
+
sessionId,
|
|
1516
|
+
),
|
|
1517
|
+
);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const graph = server.sessionManager.getTaskGraph(sessionId);
|
|
1522
|
+
|
|
1523
|
+
server._send(
|
|
1524
|
+
ws,
|
|
1525
|
+
envelopeResponse(
|
|
1526
|
+
CODING_AGENT_EVENT_TYPES.TASK_GRAPH_STATE,
|
|
1527
|
+
id,
|
|
1528
|
+
{ sessionId, graph },
|
|
1529
|
+
sessionId,
|
|
1530
|
+
),
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1167
1534
|
export function handleHostToolResult(server, id, ws, message) {
|
|
1168
1535
|
const { sessionId, requestId, success, result, error, toolName } = message;
|
|
1169
1536
|
|
package/src/lib/ws-server.js
CHANGED
|
@@ -41,6 +41,11 @@ import {
|
|
|
41
41
|
handlePatchApply,
|
|
42
42
|
handlePatchReject,
|
|
43
43
|
handlePatchSummary,
|
|
44
|
+
handleTaskGraphCreate,
|
|
45
|
+
handleTaskGraphAddNode,
|
|
46
|
+
handleTaskGraphUpdateNode,
|
|
47
|
+
handleTaskGraphAdvance,
|
|
48
|
+
handleTaskGraphState,
|
|
44
49
|
} from "../gateways/ws/session-protocol.js";
|
|
45
50
|
import {
|
|
46
51
|
handleSlashCommand,
|
|
@@ -679,6 +684,31 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
679
684
|
return handlePatchSummary(this, id, ws, message);
|
|
680
685
|
}
|
|
681
686
|
|
|
687
|
+
/** @private */
|
|
688
|
+
_handleTaskGraphCreate(id, ws, message) {
|
|
689
|
+
return handleTaskGraphCreate(this, id, ws, message);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** @private */
|
|
693
|
+
_handleTaskGraphAddNode(id, ws, message) {
|
|
694
|
+
return handleTaskGraphAddNode(this, id, ws, message);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** @private */
|
|
698
|
+
_handleTaskGraphUpdateNode(id, ws, message) {
|
|
699
|
+
return handleTaskGraphUpdateNode(this, id, ws, message);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** @private */
|
|
703
|
+
_handleTaskGraphAdvance(id, ws, message) {
|
|
704
|
+
return handleTaskGraphAdvance(this, id, ws, message);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** @private */
|
|
708
|
+
_handleTaskGraphState(id, ws, message) {
|
|
709
|
+
return handleTaskGraphState(this, id, ws, message);
|
|
710
|
+
}
|
|
711
|
+
|
|
682
712
|
/** @private — ping/pong heartbeat to detect dead connections */
|
|
683
713
|
async _ensureTaskManager() {
|
|
684
714
|
if (this._taskManager) return this._taskManager;
|
|
@@ -352,6 +352,7 @@ export class WSSessionManager {
|
|
|
352
352
|
reviewState: null,
|
|
353
353
|
pendingPatches: new Map(),
|
|
354
354
|
patchHistory: [],
|
|
355
|
+
taskGraph: null,
|
|
355
356
|
interaction: null, // Set by ws-server after creation
|
|
356
357
|
createdAt: new Date().toISOString(),
|
|
357
358
|
lastActivity: new Date().toISOString(),
|
|
@@ -464,6 +465,7 @@ export class WSSessionManager {
|
|
|
464
465
|
patchHistory: Array.isArray(metadata.patchHistory)
|
|
465
466
|
? metadata.patchHistory
|
|
466
467
|
: [],
|
|
468
|
+
taskGraph: this._hydrateTaskGraph(metadata.taskGraph),
|
|
467
469
|
interaction: null,
|
|
468
470
|
createdAt: dbSession.created_at,
|
|
469
471
|
lastActivity: new Date().toISOString(),
|
|
@@ -960,6 +962,242 @@ export class WSSessionManager {
|
|
|
960
962
|
};
|
|
961
963
|
}
|
|
962
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Create or replace the task graph for a session. A graph is a DAG of
|
|
967
|
+
* `nodes` keyed by id; each node has `{ id, title, status, dependsOn[],
|
|
968
|
+
* metadata }`. Returns the serialized graph.
|
|
969
|
+
*/
|
|
970
|
+
createTaskGraph(sessionId, payload = {}) {
|
|
971
|
+
const session = this.sessions.get(sessionId);
|
|
972
|
+
if (!session) return null;
|
|
973
|
+
|
|
974
|
+
const graphId = payload.graphId || `graph-${this._generateId()}`;
|
|
975
|
+
const now = new Date().toISOString();
|
|
976
|
+
const nodes = {};
|
|
977
|
+
const incomingNodes = Array.isArray(payload.nodes) ? payload.nodes : [];
|
|
978
|
+
for (const raw of incomingNodes) {
|
|
979
|
+
if (!raw || !raw.id) continue;
|
|
980
|
+
nodes[raw.id] = this._normalizeTaskNode(raw, now);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const graph = {
|
|
984
|
+
graphId,
|
|
985
|
+
title: payload.title || null,
|
|
986
|
+
description: payload.description || null,
|
|
987
|
+
status: "active",
|
|
988
|
+
createdAt: now,
|
|
989
|
+
updatedAt: now,
|
|
990
|
+
completedAt: null,
|
|
991
|
+
nodes,
|
|
992
|
+
order: Object.keys(nodes),
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
session.taskGraph = graph;
|
|
996
|
+
session.lastActivity = now;
|
|
997
|
+
this._persistSessionState(sessionId);
|
|
998
|
+
return this._cloneTaskGraph(graph);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Add a node to the existing task graph. Fails if no graph exists or if
|
|
1003
|
+
* the node id already exists.
|
|
1004
|
+
*/
|
|
1005
|
+
addTaskNode(sessionId, payload = {}) {
|
|
1006
|
+
const session = this.sessions.get(sessionId);
|
|
1007
|
+
if (!session || !session.taskGraph) return null;
|
|
1008
|
+
if (!payload || !payload.id) return null;
|
|
1009
|
+
const graph = session.taskGraph;
|
|
1010
|
+
if (graph.nodes[payload.id]) return null;
|
|
1011
|
+
|
|
1012
|
+
const now = new Date().toISOString();
|
|
1013
|
+
graph.nodes[payload.id] = this._normalizeTaskNode(payload, now);
|
|
1014
|
+
graph.order = [...(graph.order || []), payload.id];
|
|
1015
|
+
graph.updatedAt = now;
|
|
1016
|
+
session.lastActivity = now;
|
|
1017
|
+
this._persistSessionState(sessionId);
|
|
1018
|
+
return this._cloneTaskGraph(graph);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Update a node's status / metadata. Valid statuses: pending, ready,
|
|
1023
|
+
* running, completed, failed, skipped.
|
|
1024
|
+
*/
|
|
1025
|
+
updateTaskNode(sessionId, nodeId, updates = {}) {
|
|
1026
|
+
const session = this.sessions.get(sessionId);
|
|
1027
|
+
if (!session || !session.taskGraph) return null;
|
|
1028
|
+
const graph = session.taskGraph;
|
|
1029
|
+
const node = graph.nodes[nodeId];
|
|
1030
|
+
if (!node) return null;
|
|
1031
|
+
|
|
1032
|
+
const now = new Date().toISOString();
|
|
1033
|
+
if (updates.status) {
|
|
1034
|
+
node.status = String(updates.status);
|
|
1035
|
+
if (node.status === "running" && !node.startedAt) {
|
|
1036
|
+
node.startedAt = now;
|
|
1037
|
+
}
|
|
1038
|
+
if (
|
|
1039
|
+
node.status === "completed" ||
|
|
1040
|
+
node.status === "failed" ||
|
|
1041
|
+
node.status === "skipped"
|
|
1042
|
+
) {
|
|
1043
|
+
node.completedAt = now;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (updates.title !== undefined) node.title = updates.title;
|
|
1047
|
+
if (updates.result !== undefined) node.result = updates.result;
|
|
1048
|
+
if (updates.error !== undefined) node.error = updates.error;
|
|
1049
|
+
if (updates.metadata !== undefined) {
|
|
1050
|
+
node.metadata = { ...(node.metadata || {}), ...(updates.metadata || {}) };
|
|
1051
|
+
}
|
|
1052
|
+
node.updatedAt = now;
|
|
1053
|
+
graph.updatedAt = now;
|
|
1054
|
+
|
|
1055
|
+
// Check graph completion
|
|
1056
|
+
const allDone = Object.values(graph.nodes).every((n) =>
|
|
1057
|
+
["completed", "failed", "skipped"].includes(n.status),
|
|
1058
|
+
);
|
|
1059
|
+
if (allDone) {
|
|
1060
|
+
graph.status = Object.values(graph.nodes).some(
|
|
1061
|
+
(n) => n.status === "failed",
|
|
1062
|
+
)
|
|
1063
|
+
? "failed"
|
|
1064
|
+
: "completed";
|
|
1065
|
+
graph.completedAt = now;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
session.lastActivity = now;
|
|
1069
|
+
this._persistSessionState(sessionId);
|
|
1070
|
+
return this._cloneTaskGraph(graph);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Advance the task graph: mark any `pending` node whose dependencies are
|
|
1075
|
+
* all `completed` (or `skipped`) as `ready`. Returns the list of node ids
|
|
1076
|
+
* that became ready and the updated graph snapshot.
|
|
1077
|
+
*/
|
|
1078
|
+
advanceTaskGraph(sessionId) {
|
|
1079
|
+
const session = this.sessions.get(sessionId);
|
|
1080
|
+
if (!session || !session.taskGraph) return null;
|
|
1081
|
+
const graph = session.taskGraph;
|
|
1082
|
+
|
|
1083
|
+
const becameReady = [];
|
|
1084
|
+
for (const node of Object.values(graph.nodes)) {
|
|
1085
|
+
if (node.status !== "pending") continue;
|
|
1086
|
+
const deps = Array.isArray(node.dependsOn) ? node.dependsOn : [];
|
|
1087
|
+
const blocked = deps.some((depId) => {
|
|
1088
|
+
const dep = graph.nodes[depId];
|
|
1089
|
+
if (!dep) return true;
|
|
1090
|
+
return dep.status !== "completed" && dep.status !== "skipped";
|
|
1091
|
+
});
|
|
1092
|
+
if (!blocked) {
|
|
1093
|
+
node.status = "ready";
|
|
1094
|
+
node.updatedAt = new Date().toISOString();
|
|
1095
|
+
becameReady.push(node.id);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (becameReady.length > 0) {
|
|
1100
|
+
graph.updatedAt = new Date().toISOString();
|
|
1101
|
+
session.lastActivity = graph.updatedAt;
|
|
1102
|
+
this._persistSessionState(sessionId);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return {
|
|
1106
|
+
graph: this._cloneTaskGraph(graph),
|
|
1107
|
+
becameReady,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
getTaskGraph(sessionId) {
|
|
1112
|
+
const session = this.sessions.get(sessionId);
|
|
1113
|
+
if (!session || !session.taskGraph) return null;
|
|
1114
|
+
return this._cloneTaskGraph(session.taskGraph);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
clearTaskGraph(sessionId) {
|
|
1118
|
+
const session = this.sessions.get(sessionId);
|
|
1119
|
+
if (!session) return false;
|
|
1120
|
+
session.taskGraph = null;
|
|
1121
|
+
session.lastActivity = new Date().toISOString();
|
|
1122
|
+
this._persistSessionState(sessionId);
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
_normalizeTaskNode(raw, now) {
|
|
1127
|
+
const status = raw.status || "pending";
|
|
1128
|
+
return {
|
|
1129
|
+
id: raw.id,
|
|
1130
|
+
title: raw.title || raw.id,
|
|
1131
|
+
description: raw.description || null,
|
|
1132
|
+
status,
|
|
1133
|
+
dependsOn: Array.isArray(raw.dependsOn)
|
|
1134
|
+
? raw.dependsOn.filter((x) => typeof x === "string")
|
|
1135
|
+
: [],
|
|
1136
|
+
metadata:
|
|
1137
|
+
raw.metadata && typeof raw.metadata === "object" ? raw.metadata : {},
|
|
1138
|
+
createdAt: raw.createdAt || now,
|
|
1139
|
+
updatedAt: raw.updatedAt || now,
|
|
1140
|
+
startedAt: raw.startedAt || null,
|
|
1141
|
+
completedAt: raw.completedAt || null,
|
|
1142
|
+
result: raw.result || null,
|
|
1143
|
+
error: raw.error || null,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
_cloneTaskGraph(graph) {
|
|
1148
|
+
if (!graph) return null;
|
|
1149
|
+
return {
|
|
1150
|
+
graphId: graph.graphId,
|
|
1151
|
+
title: graph.title,
|
|
1152
|
+
description: graph.description,
|
|
1153
|
+
status: graph.status,
|
|
1154
|
+
createdAt: graph.createdAt,
|
|
1155
|
+
updatedAt: graph.updatedAt,
|
|
1156
|
+
completedAt: graph.completedAt,
|
|
1157
|
+
order: Array.isArray(graph.order)
|
|
1158
|
+
? [...graph.order]
|
|
1159
|
+
: Object.keys(graph.nodes || {}),
|
|
1160
|
+
nodes: Object.fromEntries(
|
|
1161
|
+
Object.entries(graph.nodes || {}).map(([id, node]) => [
|
|
1162
|
+
id,
|
|
1163
|
+
{
|
|
1164
|
+
...node,
|
|
1165
|
+
dependsOn: [...(node.dependsOn || [])],
|
|
1166
|
+
metadata: { ...(node.metadata || {}) },
|
|
1167
|
+
},
|
|
1168
|
+
]),
|
|
1169
|
+
),
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
_hydrateTaskGraph(data) {
|
|
1174
|
+
if (!data || typeof data !== "object") return null;
|
|
1175
|
+
if (!data.graphId || !data.nodes) return null;
|
|
1176
|
+
const nodes = {};
|
|
1177
|
+
for (const [id, node] of Object.entries(data.nodes)) {
|
|
1178
|
+
nodes[id] = this._normalizeTaskNode(
|
|
1179
|
+
{ ...node, id },
|
|
1180
|
+
node.createdAt || new Date().toISOString(),
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
graphId: data.graphId,
|
|
1185
|
+
title: data.title || null,
|
|
1186
|
+
description: data.description || null,
|
|
1187
|
+
status: data.status || "active",
|
|
1188
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
1189
|
+
updatedAt: data.updatedAt || new Date().toISOString(),
|
|
1190
|
+
completedAt: data.completedAt || null,
|
|
1191
|
+
order: Array.isArray(data.order) ? data.order : Object.keys(nodes),
|
|
1192
|
+
nodes,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
_serializeTaskGraph(graph) {
|
|
1197
|
+
if (!graph) return null;
|
|
1198
|
+
return this._cloneTaskGraph(graph);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
963
1201
|
/**
|
|
964
1202
|
* Persist current messages for a session.
|
|
965
1203
|
*/
|
|
@@ -1046,6 +1284,7 @@ export class WSSessionManager {
|
|
|
1046
1284
|
patchHistory: Array.isArray(session.patchHistory)
|
|
1047
1285
|
? session.patchHistory
|
|
1048
1286
|
: [],
|
|
1287
|
+
taskGraph: this._serializeTaskGraph(session.taskGraph),
|
|
1049
1288
|
};
|
|
1050
1289
|
}
|
|
1051
1290
|
|
|
@@ -108,6 +108,20 @@ const CODING_AGENT_EVENT_TYPES = Object.freeze({
|
|
|
108
108
|
PATCH_APPLIED: "patch.applied",
|
|
109
109
|
PATCH_REJECTED: "patch.rejected",
|
|
110
110
|
PATCH_SUMMARY: "patch.summary",
|
|
111
|
+
|
|
112
|
+
// Persistent task graph + orchestrator — a session-scoped DAG of tasks
|
|
113
|
+
// with dependencies. The runtime serializes the graph to session metadata
|
|
114
|
+
// so it survives CLI restarts; the orchestrator advances the graph by
|
|
115
|
+
// marking ready nodes as `running` when their dependencies complete.
|
|
116
|
+
TASK_GRAPH_CREATED: "task-graph.created",
|
|
117
|
+
TASK_GRAPH_UPDATED: "task-graph.updated",
|
|
118
|
+
TASK_GRAPH_NODE_ADDED: "task-graph.node.added",
|
|
119
|
+
TASK_GRAPH_NODE_UPDATED: "task-graph.node.updated",
|
|
120
|
+
TASK_GRAPH_NODE_COMPLETED: "task-graph.node.completed",
|
|
121
|
+
TASK_GRAPH_NODE_FAILED: "task-graph.node.failed",
|
|
122
|
+
TASK_GRAPH_ADVANCED: "task-graph.advanced",
|
|
123
|
+
TASK_GRAPH_COMPLETED: "task-graph.completed",
|
|
124
|
+
TASK_GRAPH_STATE: "task-graph.state",
|
|
111
125
|
});
|
|
112
126
|
|
|
113
127
|
const VALID_TYPE_SET = new Set(Object.values(CODING_AGENT_EVENT_TYPES));
|