@joeyshi12/casper 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/casper.js +7 -10
  2. package/dist/mcp.js +1 -1
  3. package/dist/server.js +388 -176
  4. package/dist/web/assets/{abnfDiagram-N423BO3Z-CqyGEOG8.js → abnfDiagram-N423BO3Z-0-vyRwna.js} +1 -1
  5. package/dist/web/assets/{arc-Co7J4_g0.js → arc-VZ5yzQqZ.js} +1 -1
  6. package/dist/web/assets/{architectureDiagram-T3A2C74G-DrmqajPn.js → architectureDiagram-T3A2C74G-BppgS1Sz.js} +1 -1
  7. package/dist/web/assets/{blockDiagram-VBNYF7ZC-BM4aGq-2.js → blockDiagram-VBNYF7ZC--t-GweDB.js} +1 -1
  8. package/dist/web/assets/{c4Diagram-5PPSVZJV-jYLQex_F.js → c4Diagram-5PPSVZJV-p6ZdSG4y.js} +1 -1
  9. package/dist/web/assets/channel-CK5ai5Le.js +1 -0
  10. package/dist/web/assets/{chunk-2GRJ4B5K-D3yOD5Jc.js → chunk-2GRJ4B5K-CJ-6WwVd.js} +1 -1
  11. package/dist/web/assets/{chunk-2Q5K7J3B-eNp6GymR.js → chunk-2Q5K7J3B-CAg8PftP.js} +1 -1
  12. package/dist/web/assets/{chunk-5RXB4S5H-T9iaxe5e.js → chunk-5RXB4S5H-DE-1UG6l.js} +1 -1
  13. package/dist/web/assets/{chunk-5VM5RSS4-BjDCtHJi.js → chunk-5VM5RSS4-BF_ZooeK.js} +1 -1
  14. package/dist/web/assets/{chunk-6Q2QTUOP-DYRDUSAT.js → chunk-6Q2QTUOP-DrFsrGLf.js} +1 -1
  15. package/dist/web/assets/{chunk-GF5L2VYU-CDkVvx8j.js → chunk-GF5L2VYU-o00BbEzr.js} +1 -1
  16. package/dist/web/assets/{chunk-JWPE2WC7-DrIE-Bh-.js → chunk-JWPE2WC7-Dmo8Zt5w.js} +1 -1
  17. package/dist/web/assets/{chunk-KBJHAD2P-DG-dHq84.js → chunk-KBJHAD2P-D-fmsXW7.js} +1 -1
  18. package/dist/web/assets/{chunk-RYQCIY6F-xj6xc7d2.js → chunk-RYQCIY6F-DsmgISQ9.js} +1 -1
  19. package/dist/web/assets/{chunk-XXDRQBXY-CjIiZdHc.js → chunk-XXDRQBXY-lfotbVIr.js} +1 -1
  20. package/dist/web/assets/classDiagram-JCYQIIEL-DNmLrfqs.js +1 -0
  21. package/dist/web/assets/classDiagram-v2-OCEON4UE-DNmLrfqs.js +1 -0
  22. package/dist/web/assets/{cose-bilkent-JH36ORCC-BcSHtUpl.js → cose-bilkent-JH36ORCC-Davnnz9g.js} +1 -1
  23. package/dist/web/assets/{cynefin-VYW2F7L2-Bj-UuS_4.js → cynefin-VYW2F7L2-CZ8lLO0w.js} +1 -1
  24. package/dist/web/assets/{cynefinDiagram-MW4NZA55-1Cd2NcM6.js → cynefinDiagram-MW4NZA55-DVNE3feo.js} +1 -1
  25. package/dist/web/assets/{dagre-VZM6K2ZE-DUX4S3MP.js → dagre-VZM6K2ZE-iF49HCxN.js} +1 -1
  26. package/dist/web/assets/{diagram-7IWD3JNH-jGdatiQR.js → diagram-7IWD3JNH-Dyxz7laM.js} +1 -1
  27. package/dist/web/assets/{diagram-B4RE2ZJO-Duop1UHS.js → diagram-B4RE2ZJO-BmK-YhIY.js} +1 -1
  28. package/dist/web/assets/{diagram-LBJQPF4R-DEFogBqA.js → diagram-LBJQPF4R-BuyX6AN9.js} +1 -1
  29. package/dist/web/assets/{diagram-Q27KOJAE-CL1d-wRv.js → diagram-Q27KOJAE-Dj7-ytuS.js} +1 -1
  30. package/dist/web/assets/{diagram-UB23O5K3-DXwhKhT2.js → diagram-UB23O5K3-CXPshI93.js} +1 -1
  31. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-Cx5whiF_.js → ebnfDiagram-BXEA7PRR-CyeZHkNl.js} +1 -1
  32. package/dist/web/assets/{erDiagram-JOGREHBK-CxxmfAXp.js → erDiagram-JOGREHBK-D7PLbZ8B.js} +1 -1
  33. package/dist/web/assets/{flowDiagram-UKHOOZJN-D9XXHTx5.js → flowDiagram-UKHOOZJN-BjdHlvsb.js} +1 -1
  34. package/dist/web/assets/{ganttDiagram-PKOTCBZU-f2WbEiXa.js → ganttDiagram-PKOTCBZU-Bgwt6E0o.js} +1 -1
  35. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-BIOj5QUF.js → gitGraphDiagram-DS77QQ5N-DIb0g66f.js} +1 -1
  36. package/dist/web/assets/index-61Vziwj9.css +1 -0
  37. package/dist/web/assets/{index-B9wyN9oZ.js → index-BRooP9_b.js} +1 -1
  38. package/dist/web/assets/index-CNtmGmSY.js +244 -0
  39. package/dist/web/assets/{infoDiagram-6WML65LV-D1qQFwzf.js → infoDiagram-6WML65LV-TD1Gql-3.js} +1 -1
  40. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-BWg4hrU2.js → ishikawaDiagram-WSZJBQD7-C_5TvaH6.js} +1 -1
  41. package/dist/web/assets/{journeyDiagram-NVQOT4AX-DKKVHX1m.js → journeyDiagram-NVQOT4AX-BbrmbPYG.js} +1 -1
  42. package/dist/web/assets/{kanban-definition-27J2QSJJ-DK7802hr.js → kanban-definition-27J2QSJJ-CzGiSu35.js} +1 -1
  43. package/dist/web/assets/{linear-j2Z2-Ts5.js → linear-CcgG3Nga.js} +1 -1
  44. package/dist/web/assets/{mermaid.core-B9t4l1LB.js → mermaid.core-Ccv-agS1.js} +4 -4
  45. package/dist/web/assets/{mindmap-definition-FAOFIHXS-MpYC1ION.js → mindmap-definition-FAOFIHXS-D4go0uBL.js} +1 -1
  46. package/dist/web/assets/{pegDiagram-VL7TDLO6-CEVJqCIu.js → pegDiagram-VL7TDLO6-DOF1eW8-.js} +1 -1
  47. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-DQDaWsnB.js → pieDiagram-7S7Q4E2Y-BhwSOyGD.js} +1 -1
  48. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-D2ovSq73.js → quadrantDiagram-CIZ2JOQS-8-4kqE4f.js} +1 -1
  49. package/dist/web/assets/{railroadDiagram-AXF67PYL-BtG5ML2m.js → railroadDiagram-AXF67PYL-Eq1262O5.js} +1 -1
  50. package/dist/web/assets/{requirementDiagram-LRYGKXZP-t8GKRNCM.js → requirementDiagram-LRYGKXZP-Ch7ZlpbD.js} +1 -1
  51. package/dist/web/assets/{sankeyDiagram-W5VNT64P-B5J2_2ZC.js → sankeyDiagram-W5VNT64P-BxmqD4QJ.js} +1 -1
  52. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-C64N2umn.js → sequenceDiagram-SI44F4Z6-ClCUZ1cS.js} +1 -1
  53. package/dist/web/assets/{sizeCapture-X5ZJPWSS-BcAjn7U8.js → sizeCapture-X5ZJPWSS-C3E5xf8i.js} +1 -1
  54. package/dist/web/assets/{stateDiagram-OKZ733FA-DkySkcyS.js → stateDiagram-OKZ733FA-7n9HeOzj.js} +1 -1
  55. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-DHHb8a_r.js +1 -0
  56. package/dist/web/assets/{swimlanes-SLNWSIFB-2PZUwW3q.js → swimlanes-SLNWSIFB-B9Zx3Udc.js} +2 -2
  57. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CW0nTs9y.js +8 -0
  58. package/dist/web/assets/{timeline-definition-Z64GVDOM-DP8smTU4.js → timeline-definition-Z64GVDOM-B_iwqTWp.js} +1 -1
  59. package/dist/web/assets/{vennDiagram-T6HMQDX7-0oTgA1su.js → vennDiagram-T6HMQDX7-DLk_W_fZ.js} +1 -1
  60. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-BTH9DrGt.js → wardleyDiagram-T6FBY63Y-CQjkYPQC.js} +1 -1
  61. package/dist/web/assets/{xychartDiagram-ELKLHX3M-CaIgazAx.js → xychartDiagram-ELKLHX3M-CHFVbcYk.js} +1 -1
  62. package/dist/web/index.html +2 -2
  63. package/dist/web/sw.js +1 -1
  64. package/package.json +1 -1
  65. package/dist/web/assets/channel-C8HH961c.js +0 -1
  66. package/dist/web/assets/classDiagram-JCYQIIEL-C-Ud8JVm.js +0 -1
  67. package/dist/web/assets/classDiagram-v2-OCEON4UE-C-Ud8JVm.js +0 -1
  68. package/dist/web/assets/index-BnkFD0g5.css +0 -1
  69. package/dist/web/assets/index-Bs2qO5Pu.js +0 -249
  70. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-Dw0_SvNt.js +0 -1
  71. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-DefMsorl.js +0 -8
package/dist/server.js CHANGED
@@ -96,9 +96,7 @@ function agentPrompt() {
96
96
  const here = path4.dirname(url.fileURLToPath(import.meta.url));
97
97
  const candidates = [
98
98
  path4.resolve(here, "agents/prompt.txt"),
99
- // bundled: beside server.js
100
99
  path4.resolve(here, "../../../assets/agents/prompt.txt")
101
- // from source
102
100
  ];
103
101
  const found = candidates.find((c) => fs3.existsSync(c));
104
102
  return found ? fs3.readFileSync(found, "utf8").trimEnd() : null;
@@ -107,9 +105,7 @@ function mcpServerPath() {
107
105
  const here = path4.dirname(url.fileURLToPath(import.meta.url));
108
106
  const candidates = [
109
107
  path4.resolve(here, "mcp.js"),
110
- // bundled: beside server.js
111
108
  path4.resolve(here, "../../dist/mcp.js")
112
- // from source, if built
113
109
  ];
114
110
  return candidates.find((c) => fs3.existsSync(c)) ?? null;
115
111
  }
@@ -314,31 +310,19 @@ var init_config = __esm({
314
310
  config = {
315
311
  host: setting("HOST", "host", "0.0.0.0"),
316
312
  port: settingInt("PORT", "port", 4319),
317
- /** Shared-secret token required to log in. Empty string disables auth (dev only). */
313
+ /** Shared secret for login. Empty disables auth (dev only). */
318
314
  token: setting("CASPER_TOKEN", "token", ""),
319
- /** Device-login lifetime in seconds; slid forward on activity. Default 7 days. */
315
+ /** Device-login lifetime in seconds, slid forward on activity. Default 7 days. */
320
316
  sessionTtlSeconds: settingInt("CASPER_SESSION_TTL_SECONDS", "sessionTtlSeconds", 60 * 60 * 24 * 7),
321
317
  kiroBin: resolveKiroBin(setting("KIRO_BIN", "kiroBin", "kiro-cli"), home),
322
318
  defaultCwd: setting("DEFAULT_CWD", "defaultCwd", process.cwd()),
323
319
  maxLiveSessions: settingInt("MAX_LIVE_SESSIONS", "maxLiveSessions", 6),
324
- /**
325
- * The agent Casper installs, which is the one with the widget tools. Safe as a
326
- * default even when it is missing: kiro-cli acp falls back to kiro_default rather
327
- * than failing, and reports what it chose, which is what a session records.
328
- */
320
+ /** The agent Casper installs, with the widget tools. Falls back to kiro_default
321
+ * if missing rather than failing. */
329
322
  defaultAgent: setting("DEFAULT_AGENT", "defaultAgent", "casper"),
330
- /**
331
- * Filesystem root the file endpoints (/api/fs/dirs, /api/fs/file) are confined to; anything
332
- * resolving outside it is rejected. Defaults to /, so browsing spans everything the process
333
- * can read - set a narrower path to keep authenticated users out of system files such as /etc
334
- * or SSH keys.
335
- */
323
+ /** Filesystem root the file endpoints are confined to. Defaults to /; narrow it
324
+ * to keep authenticated users out of system files such as /etc or SSH keys. */
336
325
  fileRoot: path5.resolve(setting("CASPER_FILE_ROOT", "fileRoot", "/")),
337
- /**
338
- * Directory where kiro-cli persists its own session files. Overridable for the
339
- * same reason as every other path here: a non-default kiro home, a container,
340
- * or a test that must not write into the real one.
341
- */
342
326
  kiroSessionsDir: path5.resolve(
343
327
  setting(
344
328
  "CASPER_KIRO_SESSIONS_DIR",
@@ -346,16 +330,10 @@ var init_config = __esm({
346
330
  path5.join(home, ".kiro", "sessions", "cli")
347
331
  )
348
332
  ),
349
- /** Where casper.db lives. Env-only: it says where data is, and the config
350
- * file isn't there. */
351
333
  casperDataDir: dataDirPath(),
352
- /** Built web app to serve. Env-only: install layout, not a user preference. */
353
334
  webDist: env("CASPER_WEB_DIST", defaultWebDist()),
354
- /** Per-session in-memory event ring buffer size. */
355
335
  eventBufferSize: settingInt("EVENT_BUFFER_SIZE", "eventBufferSize", 5e3),
356
- /** Max size (bytes) for a single uploaded file. Default 100 MB. */
357
336
  maxUploadBytes: settingInt("CASPER_MAX_UPLOAD_BYTES", "maxUploadBytes", 100 * 1024 * 1024),
358
- /** Absolute path of the settings file, for diagnostics. */
359
337
  configFile: configFilePath()
360
338
  };
361
339
  }
@@ -414,6 +392,13 @@ CREATE TABLE IF NOT EXISTS message_attachments (
414
392
  kind TEXT NOT NULL,
415
393
  PRIMARY KEY (chat_id, ordinal, path)
416
394
  );
395
+ CREATE TABLE IF NOT EXISTS subagent_links (
396
+ child_session_id TEXT PRIMARY KEY,
397
+ parent_session_id TEXT NOT NULL,
398
+ tool_call_id TEXT NOT NULL,
399
+ stage_name TEXT NOT NULL
400
+ );
401
+ CREATE INDEX IF NOT EXISTS subagent_links_parent ON subagent_links (parent_session_id);
417
402
  `;
418
403
  }
419
404
  });
@@ -451,12 +436,11 @@ var init_logins = __esm({
451
436
  ttlMs() {
452
437
  return config.sessionTtlSeconds * 1e3;
453
438
  }
454
- /** Drop logins whose last activity is older than the TTL. */
455
439
  pruneExpired(now = Date.now()) {
456
440
  const cutoff = new Date(now - this.ttlMs()).toISOString();
457
441
  db().prepare("DELETE FROM logins WHERE last_seen_at < ?").run(cutoff);
458
442
  }
459
- /** Create a login. Returns the raw token to set as the cookie value. */
443
+ /** Creates a login. Returns the raw token to set as the cookie value. */
460
444
  create(userAgent) {
461
445
  const token = randomBytes(32).toString("base64url");
462
446
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
@@ -473,10 +457,7 @@ var init_logins = __esm({
473
457
  ).run(record.id, record.hash, nowIso, nowIso, userAgent ?? null);
474
458
  return { token, record };
475
459
  }
476
- /**
477
- * Verify a raw token. Returns the record (sliding its expiry forward) or null
478
- * if unknown/expired. Writes the lastSeen bump at most once a minute.
479
- */
460
+ /** Verifies a raw token, sliding its expiry forward. Null if unknown or expired. */
480
461
  verify(token) {
481
462
  if (!token) return null;
482
463
  const now = Date.now();
@@ -493,7 +474,7 @@ var init_logins = __esm({
493
474
  }
494
475
  return record;
495
476
  }
496
- /** List all active devices, marking the one owning `currentToken`. */
477
+ /** All active devices, marking the one owning `currentToken`. */
497
478
  list(currentToken) {
498
479
  this.pruneExpired();
499
480
  const currentHash = currentToken ? sha256(currentToken) : void 0;
@@ -506,17 +487,16 @@ var init_logins = __esm({
506
487
  current: currentHash !== void 0 && sameHash(r.hash, currentHash)
507
488
  }));
508
489
  }
509
- /** Revoke the device holding this token (used on logout). */
490
+ /** Revokes the device holding this token (used on logout). */
510
491
  revokeToken(token) {
511
492
  if (!token) return;
512
493
  db().prepare("DELETE FROM logins WHERE hash = ?").run(sha256(token));
513
494
  }
514
- /** Revoke a device by its public id. Returns false if there was no such device. */
495
+ /** Revokes a device by its public id. False if there was no such device. */
515
496
  revokeId(id) {
516
497
  const res = db().prepare("DELETE FROM logins WHERE id = ?").run(id);
517
498
  return res.changes > 0;
518
499
  }
519
- /** Log out every device. */
520
500
  revokeAll() {
521
501
  db().prepare("DELETE FROM logins").run();
522
502
  }
@@ -972,7 +952,6 @@ var init_AcpClient = __esm({
972
952
  this.emit("serverRequest", msg);
973
953
  }
974
954
  }
975
- /** Send a request and await its result. */
976
955
  request(method, params, timeoutMs = 12e4) {
977
956
  if (this.closed) return Promise.reject(new Error("ACP client is closed"));
978
957
  const id = this.nextId++;
@@ -1002,12 +981,8 @@ var init_AcpClient = __esm({
1002
981
  write(msg) {
1003
982
  this.writable.write(JSON.stringify(msg) + "\n");
1004
983
  }
1005
- /**
1006
- * Reject all pending requests; call when the child process exits. The reason
1007
- * is passed through verbatim because it reaches the user as a turn_error -
1008
- * the caller supplies something readable, so don't bury it behind a prefix
1009
- * about ACP internals.
1010
- */
984
+ /** Rejects all pending requests, called when the child process exits. The
985
+ * reason passes through verbatim, since it reaches the user as a turn_error. */
1011
986
  fail(reason) {
1012
987
  this.closed = true;
1013
988
  for (const [id, pending] of this.pending) {
@@ -1088,7 +1063,7 @@ var init_KiroProcess = __esm({
1088
1063
  this.child.stdout.on("error", () => {
1089
1064
  });
1090
1065
  }
1091
- /** Perform the ACP initialize handshake. kiro's reply is not used. */
1066
+ /** The ACP initialize handshake. kiro's reply is not used. */
1092
1067
  async initialize() {
1093
1068
  await this.client.request(ACP_METHODS.initialize, {
1094
1069
  protocolVersion: 1,
@@ -1096,8 +1071,6 @@ var init_KiroProcess = __esm({
1096
1071
  fs: { readTextFile: false, writeTextFile: false },
1097
1072
  terminal: false
1098
1073
  },
1099
- // Informational only - kiro logs it and nothing reads it back, so it isn't
1100
- // worth wiring to package.json.
1101
1074
  clientInfo: { name: "casper", version: "0.5.0" }
1102
1075
  });
1103
1076
  }
@@ -1107,7 +1080,7 @@ var init_KiroProcess = __esm({
1107
1080
  loadSession(params) {
1108
1081
  return this.client.request(ACP_METHODS.sessionLoad, params);
1109
1082
  }
1110
- /** Run a prompt turn to completion. Resolves with the stop reason. */
1083
+ /** Runs a prompt turn to completion. */
1111
1084
  prompt(params) {
1112
1085
  return this.client.request(
1113
1086
  ACP_METHODS.sessionPrompt,
@@ -1130,27 +1103,17 @@ var init_KiroProcess = __esm({
1130
1103
  command: { command: command.replace(/^\//, ""), args: {} }
1131
1104
  });
1132
1105
  }
1133
- /** Cleanly shut down: close stdin (triggers kiro's graceful exit), then kill. */
1134
- /**
1135
- * Recent stderr from kiro, for attaching to a failure.
1136
- *
1137
- * A failed request doesn't always carry its cause in the JSON-RPC error - when
1138
- * it doesn't, kiro has usually written something here. Shared across turns, so
1139
- * treat it as context rather than proof.
1140
- */
1106
+ /** Recent stderr from kiro, for attaching to a failure. */
1141
1107
  stderrTail() {
1142
1108
  return this.recentStderr.join("\n").trim();
1143
1109
  }
1144
- /**
1145
- * Why the child died, in terms a user can act on. The exit code alone is
1146
- * opaque, so append what kiro printed - that's where "credentials have
1147
- * expired, run kiro-cli login" and friends show up.
1148
- */
1110
+ /** Why the child died: exit code plus whatever kiro printed. */
1149
1111
  exitReason(code, signal) {
1150
1112
  const base = signal ? `kiro-cli exited on ${signal}` : `kiro-cli exited with code ${code}`;
1151
1113
  const tail = this.recentStderr.join("\n").trim();
1152
1114
  return tail ? `${base}: ${tail}` : base;
1153
1115
  }
1116
+ // Closes stdin to trigger kiro's graceful exit, then force-kills if it lingers.
1154
1117
  dispose() {
1155
1118
  if (this.disposed) return;
1156
1119
  this.disposed = true;
@@ -1162,11 +1125,8 @@ var init_KiroProcess = __esm({
1162
1125
  if (this.child.exitCode === null) this.child.kill("SIGTERM");
1163
1126
  }, 1e3).unref();
1164
1127
  }
1165
- /**
1166
- * Dispose and resolve once the child has actually exited. kiro writes its
1167
- * session file on shutdown, so a caller that then deletes those files must
1168
- * wait for exit first, or kiro's write recreates them.
1169
- */
1128
+ /** Resolves once the child has exited. Caller must wait before deleting
1129
+ * session files, or kiro's shutdown write recreates them. */
1170
1130
  disposeAndWait(timeoutMs = 4e3) {
1171
1131
  if (this.child.exitCode !== null) {
1172
1132
  this.dispose();
@@ -1205,7 +1165,6 @@ var init_EventStore = __esm({
1205
1165
  this.chatId = chatId;
1206
1166
  this.capacity = config.eventBufferSize;
1207
1167
  }
1208
- /** Append an event, assign it the next seq, and fan it out. */
1209
1168
  append(payload) {
1210
1169
  this.seq += 1;
1211
1170
  const event = {
@@ -1219,19 +1178,15 @@ var init_EventStore = __esm({
1219
1178
  this.emit("event", event);
1220
1179
  return event;
1221
1180
  }
1222
- /** Highest assigned seq. Clients start their cursor here after a full refetch. */
1181
+ /** Highest assigned seq; clients start their cursor here after a full refetch. */
1223
1182
  head() {
1224
1183
  return this.seq;
1225
1184
  }
1226
- /** Oldest seq still in the buffer (0 if empty). */
1227
1185
  tail() {
1228
1186
  return this.buffer.length > 0 ? this.buffer[0].seq : 0;
1229
1187
  }
1230
- /**
1231
- * Events with seq > cursor, in order. Returns { events, gap } - gap is true
1232
- * when the cursor is older than the buffer tail, meaning some events were
1233
- * evicted and the client must resync (full transcript refetch).
1234
- */
1188
+ /** Events with seq > cursor. `gap` is true when the cursor is older than the
1189
+ * buffer tail, meaning evicted events the client must resync for instead. */
1235
1190
  getSince(cursor) {
1236
1191
  if (this.buffer.length === 0) {
1237
1192
  return { events: [], gap: cursor > 0 };
@@ -1294,6 +1249,153 @@ var init_TurnState = __esm({
1294
1249
  }
1295
1250
  });
1296
1251
 
1252
+ // server/src/session/SubagentTracker.ts
1253
+ var SubagentTracker;
1254
+ var init_SubagentTracker = __esm({
1255
+ "server/src/session/SubagentTracker.ts"() {
1256
+ "use strict";
1257
+ SubagentTracker = class {
1258
+ byChild = /* @__PURE__ */ new Map();
1259
+ pendingCalls = [];
1260
+ callStarted(toolCallId, stages) {
1261
+ this.pendingCalls.push({ toolCallId, stages });
1262
+ }
1263
+ callFinished(toolCallId) {
1264
+ const i = this.pendingCalls.findIndex((c) => c.toolCallId === toolCallId);
1265
+ if (i >= 0) this.pendingCalls.splice(i, 1);
1266
+ }
1267
+ callFor(stageName) {
1268
+ for (let i = this.pendingCalls.length - 1; i >= 0; i--) {
1269
+ if (this.pendingCalls[i].stages.some((s) => s.name === stageName)) return this.pendingCalls[i];
1270
+ }
1271
+ return void 0;
1272
+ }
1273
+ /** Folds a list_update into the tracked state, returning links newly resolved
1274
+ * this call, so the caller persists each one once. */
1275
+ apply(subagents, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
1276
+ const resolved = [];
1277
+ for (const info of subagents) {
1278
+ const existing = this.byChild.get(info.sessionId);
1279
+ const nowIso = now();
1280
+ const toolCallId = existing?.toolCallId ?? this.callFor(info.sessionName)?.toolCallId;
1281
+ if (toolCallId && !existing?.toolCallId) {
1282
+ resolved.push({ sessionId: info.sessionId, toolCallId, stageName: info.sessionName });
1283
+ }
1284
+ this.byChild.set(info.sessionId, {
1285
+ sessionId: info.sessionId,
1286
+ stageName: info.sessionName,
1287
+ toolCallId,
1288
+ status: info.status.type === "terminated" ? "completed" : "working",
1289
+ activity: info.status.message,
1290
+ createdAt: existing?.createdAt ?? nowIso,
1291
+ updatedAt: nowIso
1292
+ });
1293
+ }
1294
+ return resolved;
1295
+ }
1296
+ /** Started stages plus a synthetic 'pending' row for each declared stage that
1297
+ * hasn't started, so a stage waiting on a dependency still shows up. */
1298
+ listForCall(toolCallId) {
1299
+ const call = this.pendingCalls.find((c) => c.toolCallId === toolCallId);
1300
+ const started = [...this.byChild.values()].filter((s) => s.toolCallId === toolCallId);
1301
+ if (!call) return started.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1302
+ const startedNames = new Set(started.map((s) => s.stageName));
1303
+ const pending = call.stages.filter((s) => !startedNames.has(s.name)).map((s) => ({
1304
+ sessionId: `pending:${toolCallId}:${s.name}`,
1305
+ stageName: s.name,
1306
+ toolCallId,
1307
+ status: "pending",
1308
+ activity: s.depends_on?.length ? `Waits for ${s.depends_on.join(" and ")}` : void 0,
1309
+ createdAt: "",
1310
+ updatedAt: ""
1311
+ }));
1312
+ return [...started.sort((a, b) => a.createdAt.localeCompare(b.createdAt)), ...pending];
1313
+ }
1314
+ list() {
1315
+ return [...this.byChild.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1316
+ }
1317
+ get(sessionId) {
1318
+ return this.byChild.get(sessionId);
1319
+ }
1320
+ };
1321
+ }
1322
+ });
1323
+
1324
+ // server/src/session/subagentResult.ts
1325
+ function showSummaryAsAnswer(transcript) {
1326
+ const out = [];
1327
+ for (const item of transcript) {
1328
+ if (item.type === "message" && item.message.role === "assistant" && item.message.text.trim() === INTERRUPTED) {
1329
+ continue;
1330
+ }
1331
+ if (item.type === "tool_call" && item.tool.name === "summary") {
1332
+ const input = item.tool.input;
1333
+ const result = typeof input?.taskResult === "string" ? input.taskResult.trim() : "";
1334
+ if (result) out.push({ type: "message", message: { id: item.tool.id, role: "assistant", text: result } });
1335
+ continue;
1336
+ }
1337
+ out.push(item);
1338
+ }
1339
+ return out;
1340
+ }
1341
+ var INTERRUPTED;
1342
+ var init_subagentResult = __esm({
1343
+ "server/src/session/subagentResult.ts"() {
1344
+ "use strict";
1345
+ INTERRUPTED = "Tool uses were interrupted, waiting for the next user prompt";
1346
+ }
1347
+ });
1348
+
1349
+ // server/src/session/subagentFallbackMatch.ts
1350
+ function isStageInput(v) {
1351
+ return typeof v === "object" && v !== null && typeof v.name === "string";
1352
+ }
1353
+ function subagentCallInput(tool) {
1354
+ if (tool.name !== "subagent") return null;
1355
+ const input = tool.input;
1356
+ if (!input || !Array.isArray(input.stages)) return null;
1357
+ const stages = input.stages.filter(isStageInput);
1358
+ return { task: typeof input.task === "string" ? input.task : void 0, stages };
1359
+ }
1360
+ function expectedTitle(call, stage) {
1361
+ const task = call.task ?? "";
1362
+ const expanded = stage.prompt_template ? stage.prompt_template.replace("{task}", task) : task;
1363
+ return expanded.slice(0, TITLE_MAX_LENGTH);
1364
+ }
1365
+ function subagentCallsIn(transcript) {
1366
+ const calls = [];
1367
+ transcript.forEach((item, itemIndex) => {
1368
+ if (item.type !== "tool_call") return;
1369
+ const input = subagentCallInput(item.tool);
1370
+ if (!input) return;
1371
+ calls.push({ toolCallId: item.tool.id, itemIndex, input });
1372
+ });
1373
+ return calls;
1374
+ }
1375
+ function matchSubagentsFallback(calls, children) {
1376
+ const sortedCalls = [...calls].sort((a, b) => a.itemIndex - b.itemIndex);
1377
+ const unclaimed = [...children].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1378
+ const matches = [];
1379
+ for (const call of sortedCalls) {
1380
+ for (const stage of call.input.stages) {
1381
+ const want = expectedTitle(call.input, stage);
1382
+ if (!want) continue;
1383
+ const i = unclaimed.findIndex((c) => want.startsWith(c.title) || c.title.startsWith(want));
1384
+ if (i < 0) continue;
1385
+ const child = unclaimed.splice(i, 1)[0];
1386
+ matches.push({ sessionId: child.sessionId, toolCallId: call.toolCallId, stageName: stage.name });
1387
+ }
1388
+ }
1389
+ return matches;
1390
+ }
1391
+ var TITLE_MAX_LENGTH;
1392
+ var init_subagentFallbackMatch = __esm({
1393
+ "server/src/session/subagentFallbackMatch.ts"() {
1394
+ "use strict";
1395
+ TITLE_MAX_LENGTH = 150;
1396
+ }
1397
+ });
1398
+
1297
1399
  // server/src/session/kiroFiles.ts
1298
1400
  import fs9 from "node:fs/promises";
1299
1401
  import path10 from "node:path";
@@ -1329,7 +1431,6 @@ function summarize(j) {
1329
1431
  const contextUsagePercentage = state?.rts_model_state?.context_usage_percentage ?? turns[turns.length - 1]?.context_usage_percentage;
1330
1432
  return {
1331
1433
  sessionId: j.session_id,
1332
- // Left empty when kiro has not named it; resolveSessionTitle decides what to show.
1333
1434
  title: j.title?.trim() ?? "",
1334
1435
  cwd: j.cwd,
1335
1436
  createdAt: j.created_at,
@@ -1362,6 +1463,29 @@ async function readPersistedSession(sessionId) {
1362
1463
  return null;
1363
1464
  }
1364
1465
  }
1466
+ async function listChildSessions(parentSessionId) {
1467
+ if (!isValidSessionId(parentSessionId)) return [];
1468
+ let names;
1469
+ try {
1470
+ names = await fs9.readdir(config.kiroSessionsDir);
1471
+ } catch {
1472
+ return [];
1473
+ }
1474
+ const out = [];
1475
+ await Promise.all(
1476
+ names.filter((n) => n.endsWith(".json")).map(async (n) => {
1477
+ try {
1478
+ const raw = await fs9.readFile(path10.join(config.kiroSessionsDir, n), "utf8");
1479
+ const j = JSON.parse(raw);
1480
+ if (j.parent_session_id === parentSessionId) {
1481
+ out.push({ ...summarize(j), parentSessionId });
1482
+ }
1483
+ } catch {
1484
+ }
1485
+ })
1486
+ );
1487
+ return out;
1488
+ }
1365
1489
  async function promptCount(sessionId) {
1366
1490
  const entries = await readJsonlEntries(sessionId);
1367
1491
  return entries.filter((e) => e.kind === "Prompt").length;
@@ -1466,7 +1590,6 @@ var init_chatStore = __esm({
1466
1590
  "use strict";
1467
1591
  init_db();
1468
1592
  ChatStore = class {
1469
- /** Every chat. Ordering is kiro's business, applied once the file is joined on. */
1470
1593
  all() {
1471
1594
  return db().prepare("SELECT chat_id, session_id, title, cwd FROM chats").all().map((r) => ({ chatId: r.chat_id, sessionId: r.session_id, title: r.title, cwd: r.cwd }));
1472
1595
  }
@@ -1477,10 +1600,8 @@ var init_chatStore = __esm({
1477
1600
  create(chatId) {
1478
1601
  db().prepare("INSERT INTO chats (chat_id) VALUES (?) ON CONFLICT(chat_id) DO NOTHING").run(chatId);
1479
1602
  }
1480
- /**
1481
- * kiro names the session once it starts one; bind it to the chat that owns it. A session
1482
- * belongs to exactly one chat, so any earlier claim on it is released.
1483
- */
1603
+ /** Binds the session kiro named once it started one. A session belongs to
1604
+ * exactly one chat, so any earlier claim on it is released. */
1484
1605
  bindSession(chatId, sessionId) {
1485
1606
  db().prepare("UPDATE chats SET session_id = NULL WHERE session_id = ? AND chat_id <> ?").run(sessionId, chatId);
1486
1607
  this.write(chatId, "session_id", sessionId);
@@ -1501,7 +1622,7 @@ var init_chatStore = __esm({
1501
1622
  setCwd(chatId, cwd) {
1502
1623
  this.write(chatId, "cwd", cwd);
1503
1624
  }
1504
- /** Record what was attached to one prompt. See db.ts for why the key is an ordinal. */
1625
+ /** What was attached to one prompt, keyed by ordinal (see db.ts). */
1505
1626
  setAttachments(chatId, ordinal, files) {
1506
1627
  if (files.length === 0) return;
1507
1628
  const stmt = db().prepare(
@@ -1550,6 +1671,38 @@ var init_chatStore = __esm({
1550
1671
  }
1551
1672
  });
1552
1673
 
1674
+ // server/src/session/subagentLinks.ts
1675
+ var SubagentLinkStore;
1676
+ var init_subagentLinks = __esm({
1677
+ "server/src/session/subagentLinks.ts"() {
1678
+ "use strict";
1679
+ init_db();
1680
+ SubagentLinkStore = class {
1681
+ /** Record (or correct) one child's link. Idempotent: a repeat call with the same
1682
+ * values is a no-op, and the row always reflects the latest call to resolve it. */
1683
+ record(childSessionId, parentSessionId, toolCallId, stageName) {
1684
+ db().prepare(
1685
+ `INSERT INTO subagent_links (child_session_id, parent_session_id, tool_call_id, stage_name)
1686
+ VALUES (?, ?, ?, ?)
1687
+ ON CONFLICT(child_session_id) DO UPDATE SET
1688
+ parent_session_id = excluded.parent_session_id,
1689
+ tool_call_id = excluded.tool_call_id,
1690
+ stage_name = excluded.stage_name`
1691
+ ).run(childSessionId, parentSessionId, toolCallId, stageName);
1692
+ }
1693
+ /** Every link recorded for one parent session's children, keyed by child session id. */
1694
+ forParent(parentSessionId) {
1695
+ const rows = db().prepare(
1696
+ "SELECT child_session_id, tool_call_id, stage_name FROM subagent_links WHERE parent_session_id = ?"
1697
+ ).all(parentSessionId);
1698
+ const out = /* @__PURE__ */ new Map();
1699
+ for (const r of rows) out.set(r.child_session_id, { toolCallId: r.tool_call_id, stageName: r.stage_name });
1700
+ return out;
1701
+ }
1702
+ };
1703
+ }
1704
+ });
1705
+
1553
1706
  // server/src/session/SessionManager.ts
1554
1707
  import fs10 from "node:fs";
1555
1708
  import path11 from "node:path";
@@ -1585,6 +1738,22 @@ function mapNotification(n) {
1585
1738
  return null;
1586
1739
  }
1587
1740
  }
1741
+ function notificationSessionId(n) {
1742
+ const params = n.params;
1743
+ return params?.sessionId;
1744
+ }
1745
+ function subagentCallStages(update) {
1746
+ if (update.sessionUpdate !== "tool_call") return null;
1747
+ const meta = update._meta;
1748
+ if (meta?.kiro?.toolName !== "subagent") return null;
1749
+ const toolCallId = update.toolCallId;
1750
+ const rawInput = update.rawInput;
1751
+ if (typeof toolCallId !== "string" || !Array.isArray(rawInput?.stages)) return null;
1752
+ const stages = rawInput.stages.filter(
1753
+ (s) => typeof s === "object" && s !== null && typeof s.name === "string"
1754
+ );
1755
+ return { toolCallId, stages };
1756
+ }
1588
1757
  function newerOf(a, b) {
1589
1758
  return a && a.localeCompare(b) > 0 ? a : b;
1590
1759
  }
@@ -1607,38 +1776,34 @@ var init_SessionManager = __esm({
1607
1776
  init_KiroProcess();
1608
1777
  init_EventStore();
1609
1778
  init_TurnState();
1779
+ init_SubagentTracker();
1780
+ init_subagentResult();
1781
+ init_subagentFallbackMatch();
1610
1782
  init_kiroFiles();
1611
1783
  init_chatStore();
1784
+ init_subagentLinks();
1612
1785
  Session = class {
1613
1786
  sessionId;
1614
1787
  store;
1615
1788
  turnState = new TurnState();
1789
+ subagents = new SubagentTracker();
1616
1790
  cwd;
1617
1791
  agentId;
1618
1792
  modelId;
1619
1793
  currentModeId;
1620
1794
  availableModes = [];
1621
- /** Empty until something names it; resolveSessionTitle owns the fallback. */
1622
1795
  title = "";
1623
1796
  createdAt = (/* @__PURE__ */ new Date()).toISOString();
1624
1797
  updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1625
1798
  lastActivity = Date.now();
1626
1799
  running = false;
1627
- // True once kiro has created or loaded this session id.
1628
1800
  everLive = false;
1629
1801
  proc;
1630
- // In-flight spawn, so concurrent actions share one process.
1631
1802
  spawning;
1632
- /**
1633
- * Set while this session's process is being replaced, and resolved when the
1634
- * replacement is ready. Claimed synchronously with the reload's guard, so an
1635
- * action arriving during one waits for the new process instead of acting on the
1636
- * one about to be disposed.
1637
- */
1803
+ // An action during a reload waits on this instead of acting on the process
1804
+ // about to be disposed.
1638
1805
  reloading;
1639
- // True while kiro is replaying history during session/load. The transcript is
1640
- // already hydrated from disk, so replayed notifications must not be appended
1641
- // to the live store (doing so floods the chat with duplicate tool calls).
1806
+ // True while kiro replays history during session/load.
1642
1807
  replaying = false;
1643
1808
  constructor(sessionId, store, cwd) {
1644
1809
  this.sessionId = sessionId;
@@ -1651,12 +1816,7 @@ var init_SessionManager = __esm({
1651
1816
  get hasBeenLive() {
1652
1817
  return this.everLive;
1653
1818
  }
1654
- /**
1655
- * Single append path: fold the event into the live snapshot AND persist/fan
1656
- * it out. Using this everywhere keeps turnState in sync with the event log,
1657
- * so a client that refetches mid-turn sees turnStatus 'running' rather than
1658
- * a stale 'idle'.
1659
- */
1819
+ // Folds the event into the live snapshot and persists it in one call.
1660
1820
  record(payload) {
1661
1821
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1662
1822
  this.turnState.apply(payload);
@@ -1668,15 +1828,13 @@ var init_SessionManager = __esm({
1668
1828
  sessions = /* @__PURE__ */ new Map();
1669
1829
  log;
1670
1830
  store = new ChatStore();
1831
+ subagentLinks = new SubagentLinkStore();
1671
1832
  spawnProcess;
1672
1833
  constructor(log, opts = {}) {
1673
1834
  this.log = log;
1674
1835
  this.spawnProcess = opts.spawn ?? ((o, l) => new KiroProcess(o, l));
1675
1836
  }
1676
- /**
1677
- * What a session is called, from every read path. The folder is skipped for a workspace of
1678
- * ours, whose name is a uuid.
1679
- */
1837
+ /** What a session is called, from every read path. */
1680
1838
  titleOf(chat, parts) {
1681
1839
  return resolveSessionTitle({
1682
1840
  override: chat.title ?? void 0,
@@ -1691,10 +1849,8 @@ var init_SessionManager = __esm({
1691
1849
  const s = this.sessions.get(this.store.sessionIdForChat(chatId) ?? "");
1692
1850
  if (s) s.title = clean || s.title;
1693
1851
  }
1694
- /**
1695
- * Re-point a session at a different working directory. Any live process was spawned with the
1696
- * old cwd, so it is disposed and the next turn respawns in the new one, transcript intact.
1697
- */
1852
+ // A live process was spawned with the old cwd, so it's disposed and the next
1853
+ // turn respawns.
1698
1854
  async setChatCwd(chatId, input) {
1699
1855
  const resolved = resolveCwd(input);
1700
1856
  const s = await this.ensureOpen(chatId);
@@ -1716,15 +1872,8 @@ var init_SessionManager = __esm({
1716
1872
  }
1717
1873
  return resolved;
1718
1874
  }
1719
- /**
1720
- * Restart the session's kiro child, so what kiro reads only at startup is read again: the
1721
- * agent definition, the workspace's `.kiro`, and the MCP servers kiro launches itself.
1722
- * Replacing the process is the only way - no ACP method does it, and `session/set_mode`
1723
- * leaves those MCP servers alone.
1724
- *
1725
- * The old child must be awaited out first: kiro flushes its session file on shutdown and
1726
- * the replacement reads that file to reload the conversation.
1727
- */
1875
+ /** Restarts the session's kiro child so startup-only state (agent definition,
1876
+ * workspace `.kiro`, MCP servers) is read again. */
1728
1877
  async reloadChat(chatId) {
1729
1878
  const s = await this.ensureOpen(chatId);
1730
1879
  if (s.running) {
@@ -1747,7 +1896,7 @@ var init_SessionManager = __esm({
1747
1896
  ready();
1748
1897
  }
1749
1898
  }
1750
- /** The reload itself. Only ever called with the session's reload claim held. */
1899
+ /** The reload itself, called with the session's reload claim held. */
1751
1900
  async replaceProcess(chatId, s) {
1752
1901
  if (!await hasRecordedTurns(s.sessionId)) {
1753
1902
  throw new Error(
@@ -1769,10 +1918,6 @@ var init_SessionManager = __esm({
1769
1918
  this.log.info({ sessionId: s.sessionId, cwd: s.cwd }, "session reloaded");
1770
1919
  return this.getDetail(chatId);
1771
1920
  }
1772
- /**
1773
- * Wait out a reload before touching the session's process. Without this an action
1774
- * can be handed the process that the reload is about to dispose.
1775
- */
1776
1921
  async settleReload(s) {
1777
1922
  if (s.reloading) await s.reloading;
1778
1923
  }
@@ -1781,9 +1926,6 @@ var init_SessionManager = __esm({
1781
1926
  for (const s of this.sessions.values()) if (s.proc) n++;
1782
1927
  return n;
1783
1928
  }
1784
- // -------------------------------------------------------------------------
1785
- // Event subscription - works for any opened session, spawned or not.
1786
- // -------------------------------------------------------------------------
1787
1929
  onEvent(chatId, cb) {
1788
1930
  const s = this.sessions.get(this.store.sessionIdForChat(chatId) ?? "");
1789
1931
  if (!s) return null;
@@ -1793,12 +1935,12 @@ var init_SessionManager = __esm({
1793
1935
  getStore(chatId) {
1794
1936
  return this.sessions.get(this.store.sessionIdForChat(chatId) ?? "")?.store;
1795
1937
  }
1796
- /** A chat's working directory. Opens it in memory if needed. */
1938
+ /** A chat's working directory, opened in memory if needed. */
1797
1939
  async getChatCwd(chatId) {
1798
1940
  const s = await this.ensureOpen(chatId);
1799
1941
  return s.cwd;
1800
1942
  }
1801
- /** Open a chat's session in memory (store + metadata) WITHOUT spawning a process. */
1943
+ /** Opens a chat's session in memory without spawning a process. */
1802
1944
  async ensureOpen(chatId) {
1803
1945
  const sessionId = this.sessionIdOf(chatId);
1804
1946
  const existing = this.sessions.get(sessionId);
@@ -1824,15 +1966,33 @@ var init_SessionManager = __esm({
1824
1966
  this.sessions.set(sessionId, s);
1825
1967
  return s;
1826
1968
  }
1827
- // -------------------------------------------------------------------------
1828
- // Lazy process spawning
1829
- // -------------------------------------------------------------------------
1830
1969
  wire(s, proc) {
1831
1970
  proc.on("notification", (n) => {
1832
1971
  if (s.replaying) return;
1833
- const payload = mapNotification(n);
1834
- if (!payload) return;
1835
- s.record(payload);
1972
+ if (n.method === KIRO_NOTIFICATIONS.subagentListUpdate) {
1973
+ const params = n.params;
1974
+ const resolved = s.subagents.apply(params.subagents);
1975
+ for (const link of resolved) {
1976
+ this.subagentLinks.record(link.sessionId, s.sessionId, link.toolCallId, link.stageName);
1977
+ }
1978
+ s.record({ kind: "subagents_changed", subagents: s.subagents.list() });
1979
+ return;
1980
+ }
1981
+ const notifSessionId = notificationSessionId(n);
1982
+ if (!notifSessionId || notifSessionId === s.sessionId) {
1983
+ const payload = mapNotification(n);
1984
+ if (!payload) return;
1985
+ if (payload.kind === "session_update") {
1986
+ const call = subagentCallStages(payload.update);
1987
+ if (call) s.subagents.callStarted(call.toolCallId, call.stages);
1988
+ const update = payload.update;
1989
+ if (update.sessionUpdate === "tool_call_update" && typeof update.toolCallId === "string" && (update.status === "completed" || update.status === "failed")) {
1990
+ s.subagents.callFinished(update.toolCallId);
1991
+ }
1992
+ }
1993
+ s.record(payload);
1994
+ return;
1995
+ }
1836
1996
  });
1837
1997
  proc.on("exit", (code, signal) => {
1838
1998
  if (s.proc !== proc) return;
@@ -1841,7 +2001,7 @@ var init_SessionManager = __esm({
1841
2001
  s.running = false;
1842
2002
  });
1843
2003
  }
1844
- /** Get (or spawn + initialize + create/load) the kiro process for a session. */
2004
+ /** Gets or spawns the kiro process for a session. */
1845
2005
  async ensureProc(s) {
1846
2006
  if (s.proc) return s.proc;
1847
2007
  if (s.spawning) return s.spawning;
@@ -1903,10 +2063,7 @@ var init_SessionManager = __esm({
1903
2063
  this.log.warn("at capacity but all processes are busy");
1904
2064
  }
1905
2065
  }
1906
- // -------------------------------------------------------------------------
1907
- // Creating / opening
1908
- // -------------------------------------------------------------------------
1909
- /** Create a new session. Spawns immediately so we get a real kiro sessionId. */
2066
+ /** Creates a new session. Spawns immediately to get a real kiro sessionId. */
1910
2067
  async createChat(opts) {
1911
2068
  const chatId = isValidChatId(opts.chatId) ? opts.chatId : crypto.randomUUID();
1912
2069
  const cwd = opts.freshWorkspace ? createChatWorkspace(chatId) : resolveCwd(opts.cwd);
@@ -1932,9 +2089,6 @@ var init_SessionManager = __esm({
1932
2089
  }
1933
2090
  return this.buildDetail(this.store.get(chatId), s, []);
1934
2091
  }
1935
- // -------------------------------------------------------------------------
1936
- // Actions - these spawn the process lazily.
1937
- // -------------------------------------------------------------------------
1938
2092
  async runPrompt(chatId, content, attachments) {
1939
2093
  const s = await this.ensureOpen(chatId);
1940
2094
  await this.settleReload(s);
@@ -2003,14 +2157,8 @@ ${tail}` : err.message;
2003
2157
  await proc.execCommand(s.sessionId, command);
2004
2158
  s.lastActivity = Date.now();
2005
2159
  }
2006
- // -------------------------------------------------------------------------
2007
- // Listing / detail - never spawns.
2008
- // -------------------------------------------------------------------------
2009
- /**
2010
- * The one place a ChatSummary is assembled: kiro's file and Casper's live state each hold
2011
- * part of the truth, so every read path projects them through here. `persisted` is absent for
2012
- * a session with no file yet, `live` for a dormant one, and callers guarantee one of them.
2013
- */
2160
+ /** The one place a ChatSummary is assembled, since kiro's file and Casper's
2161
+ * live state each hold part of the truth. */
2014
2162
  summaryOf(chat, live, persisted, transcript) {
2015
2163
  const snap = live?.turnState.get();
2016
2164
  const cwd = live?.cwd ?? chat.cwd ?? persisted?.cwd ?? config.defaultCwd;
@@ -2018,21 +2166,18 @@ ${tail}` : err.message;
2018
2166
  chatId: chat.chatId,
2019
2167
  sessionId: live?.sessionId ?? chat.sessionId ?? void 0,
2020
2168
  title: this.titleOf(chat, {
2021
- // kiro's file is what kiro called it; the live copy is only a cache of it.
2022
2169
  kiroTitle: persisted?.title || live?.title,
2023
2170
  firstPrompt: transcript && firstPromptText(transcript),
2024
2171
  cwd
2025
2172
  }),
2026
2173
  cwd,
2027
2174
  createdAt: persisted?.createdAt ?? live?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2028
- // kiro's file and our own activity move separately.
2029
2175
  updatedAt: live ? newerOf(persisted?.updatedAt, live.updatedAt) : persisted?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2030
2176
  liveness: live?.proc ? "live" : "dormant",
2031
2177
  agentId: live?.agentId ?? persisted?.agentId,
2032
2178
  modelId: live?.modelId ?? persisted?.modelId,
2033
2179
  running: live?.running ?? false,
2034
- // A live snapshot starts at zero until the first turn reports, so a dormant
2035
- // session's value comes from kiro's file.
2180
+ // A dormant session falls back to kiro's file.
2036
2181
  contextUsagePercentage: snap?.contextUsagePercentage || persisted?.contextUsagePercentage
2037
2182
  };
2038
2183
  }
@@ -2072,11 +2217,8 @@ ${tail}` : err.message;
2072
2217
  if (!s && !persisted) throw new Error(`Unknown chat: ${chatId}`);
2073
2218
  return this.buildDetail(chat, s, transcript, persisted ?? void 0);
2074
2219
  }
2075
- /**
2076
- * A slice of the transcript for lazy "load older on scroll up". Re-hydrates
2077
- * from disk (fast: ~150ms even for a multi-MB session) and returns the
2078
- * requested window. offset/limit are clamped to the transcript bounds.
2079
- */
2220
+ /** A transcript slice for lazy "load older on scroll up". offset/limit are
2221
+ * clamped to the transcript bounds. */
2080
2222
  async getTranscriptPage(chatId, offset, limit) {
2081
2223
  const transcript = await hydrateTranscript(
2082
2224
  this.sessionIdOf(chatId),
@@ -2086,13 +2228,58 @@ ${tail}` : err.message;
2086
2228
  const end = Math.max(start, Math.min(start + limit, transcript.length));
2087
2229
  return transcript.slice(start, end);
2088
2230
  }
2231
+ /**
2232
+ * A chat's subagents: live ones from the tracker, joined with children found on disk.
2233
+ * A child the tracker never saw falls back to a recorded link or title matching
2234
+ * (subagentFallbackMatch.ts).
2235
+ */
2236
+ async getSubagents(chatId) {
2237
+ const sessionId = this.sessionIdOf(chatId);
2238
+ const s = this.sessions.get(sessionId);
2239
+ const live = s ? s.subagents.list() : [];
2240
+ const liveToolCalls = new Set(live.map((a) => a.toolCallId).filter((id) => !!id));
2241
+ const pending = s ? [...liveToolCalls].flatMap((id) => s.subagents.listForCall(id).filter((a) => a.status === "pending")) : [];
2242
+ const onDisk = await listChildSessions(sessionId);
2243
+ const links = this.subagentLinks.forParent(sessionId);
2244
+ const liveIds = new Set(live.map((a) => a.sessionId));
2245
+ const unresolved = onDisk.filter((c) => !liveIds.has(c.sessionId) && !links.has(c.sessionId));
2246
+ const fallback = unresolved.length ? matchSubagentsFallback(subagentCallsIn(await hydrateTranscript(sessionId)), unresolved) : [];
2247
+ const fallbackByChild = new Map(fallback.map((m) => [m.sessionId, m]));
2248
+ const byId = /* @__PURE__ */ new Map();
2249
+ for (const child of onDisk) {
2250
+ const link = links.get(child.sessionId) ?? fallbackByChild.get(child.sessionId);
2251
+ byId.set(child.sessionId, {
2252
+ sessionId: child.sessionId,
2253
+ stageName: link?.stageName ?? (child.title || child.sessionId),
2254
+ toolCallId: link?.toolCallId,
2255
+ status: "completed",
2256
+ createdAt: child.createdAt,
2257
+ updatedAt: child.updatedAt
2258
+ });
2259
+ }
2260
+ for (const a of live) byId.set(a.sessionId, a);
2261
+ const startedThenPending = [...byId.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2262
+ return [...startedThenPending, ...pending];
2263
+ }
2264
+ /** One subagent's own transcript, hydrated the same way a chat's is. */
2265
+ async getSubagentDetail(chatId, childSessionId) {
2266
+ const parentSessionId = this.sessionIdOf(chatId);
2267
+ const subagents = await this.getSubagents(chatId);
2268
+ const summary = subagents.find((a) => a.sessionId === childSessionId);
2269
+ if (!summary) throw new Error(`Unknown subagent: ${childSessionId}`);
2270
+ const onDisk = await listChildSessions(parentSessionId);
2271
+ if (!onDisk.some((c) => c.sessionId === childSessionId)) {
2272
+ throw new Error(`Unknown subagent: ${childSessionId}`);
2273
+ }
2274
+ const transcript = showSummaryAsAnswer(await hydrateTranscript(childSessionId));
2275
+ return { subagent: summary, transcript };
2276
+ }
2089
2277
  /** One projection, so a dormant session and a live one cannot disagree. */
2090
2278
  buildDetail(chat, s, transcript, persisted) {
2091
2279
  return {
2092
2280
  summary: this.summaryOf(chat, s, persisted, transcript),
2093
2281
  modes: s?.availableModes ?? [],
2094
2282
  currentModeId: s?.currentModeId ?? persisted?.agentId,
2095
- // Only the tail is sent on load; replayHead/title use the full transcript.
2096
2283
  transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
2097
2284
  transcriptTotal: transcript.length,
2098
2285
  observability: s?.turnState.get() ?? {
@@ -2103,9 +2290,8 @@ ${tail}` : err.message;
2103
2290
  };
2104
2291
  }
2105
2292
  /**
2106
- * The cursor a reconnecting client starts from. Normally the store head, but kiro writes a turn
2107
- * to its jsonl only once it completes, so a hydrated transcript is missing one in flight:
2108
- * rewind to just before its turn_started and let the socket replay the whole turn.
2293
+ * The cursor a reconnecting client starts from: the store head, unless the in-flight
2294
+ * turn isn't in kiro's jsonl yet, in which case rewind to its turn_started.
2109
2295
  */
2110
2296
  replayHead(s, transcript) {
2111
2297
  const head = s.store.head();
@@ -2131,9 +2317,6 @@ ${tail}` : err.message;
2131
2317
  }
2132
2318
  return started.seq - 1;
2133
2319
  }
2134
- // -------------------------------------------------------------------------
2135
- // Teardown
2136
- // -------------------------------------------------------------------------
2137
2320
  evict(sessionId) {
2138
2321
  const s = this.sessions.get(sessionId);
2139
2322
  if (!s) return;
@@ -2141,8 +2324,7 @@ ${tail}` : err.message;
2141
2324
  s.proc?.dispose();
2142
2325
  s.store.dispose();
2143
2326
  }
2144
- // Permanently delete a session: evict it from memory, remove its on-disk
2145
- // files and the directory the chat owns, and drop any title override.
2327
+ // Evicts from memory, removes on-disk files and the chat's directory.
2146
2328
  async deleteChat(chatId) {
2147
2329
  const sessionId = this.store.sessionIdForChat(chatId);
2148
2330
  const s = sessionId ? this.sessions.get(sessionId) : void 0;
@@ -2946,6 +3128,37 @@ var init_chats2 = __esm({
2946
3128
  }
2947
3129
  });
2948
3130
 
3131
+ // server/src/routes/subagents.ts
3132
+ function registerSubagentRoutes(app, manager) {
3133
+ app.get(
3134
+ "/api/chats/:id/subagents",
3135
+ async (req, reply) => {
3136
+ try {
3137
+ return { subagents: await manager.getSubagents(req.params.id) };
3138
+ } catch (err) {
3139
+ reply.code(404);
3140
+ return { error: err.message };
3141
+ }
3142
+ }
3143
+ );
3144
+ app.get(
3145
+ "/api/chats/:id/subagents/:subagentId",
3146
+ async (req, reply) => {
3147
+ try {
3148
+ return await manager.getSubagentDetail(req.params.id, req.params.subagentId);
3149
+ } catch (err) {
3150
+ reply.code(404);
3151
+ return { error: err.message };
3152
+ }
3153
+ }
3154
+ );
3155
+ }
3156
+ var init_subagents = __esm({
3157
+ "server/src/routes/subagents.ts"() {
3158
+ "use strict";
3159
+ }
3160
+ });
3161
+
2949
3162
  // server/src/routes/workspace.ts
2950
3163
  import fs14 from "node:fs/promises";
2951
3164
  import { createReadStream as createReadStream3 } from "node:fs";
@@ -3140,9 +3353,6 @@ function registerUploadRoutes(app) {
3140
3353
  const kind = classifyKind(dest);
3141
3354
  const uploaded = {
3142
3355
  name: path17.basename(dest),
3143
- // Absolute, because the file sits outside the session's workspace: a
3144
- // relative path would resolve against the wrong root for both the agent
3145
- // and the image endpoint.
3146
3356
  path: dest,
3147
3357
  size: stat.size,
3148
3358
  mimeType: mimeForExt(ext),
@@ -3421,11 +3631,11 @@ import multipart from "@fastify/multipart";
3421
3631
  async function buildApp() {
3422
3632
  const isProd = process.env.NODE_ENV === "production";
3423
3633
  const app = Fastify({
3424
- // Honor X-Forwarded-Proto so `secure: 'auto'` cookies detect HTTPS when the
3425
- // server sits behind a TLS-terminating tunnel or reverse proxy.
3634
+ // Honors X-Forwarded-Proto so secure: 'auto' cookies detect HTTPS behind a
3635
+ // TLS-terminating tunnel or reverse proxy.
3426
3636
  trustProxy: true,
3427
- // In production, skip the two-lines-per-request access log; our own
3428
- // logger.info/warn/error calls still fire. Dev keeps it for debugging.
3637
+ // Skip the two-lines-per-request access log in production; logger.info/warn/error
3638
+ // calls still fire. Dev keeps it for debugging.
3429
3639
  disableRequestLogging: isProd,
3430
3640
  logger: {
3431
3641
  level: process.env.LOG_LEVEL ?? "info",
@@ -3446,6 +3656,7 @@ async function buildApp() {
3446
3656
  registerAgentRoutes(app);
3447
3657
  registerFsRoutes(app);
3448
3658
  registerChatRoutes(app, manager);
3659
+ registerSubagentRoutes(app, manager);
3449
3660
  registerWorkspaceRoutes(app, manager);
3450
3661
  registerUploadRoutes(app);
3451
3662
  registerWsGateway(app, manager);
@@ -3482,6 +3693,7 @@ var init_app = __esm({
3482
3693
  init_agents2();
3483
3694
  init_fs();
3484
3695
  init_chats2();
3696
+ init_subagents();
3485
3697
  init_workspace();
3486
3698
  init_uploads();
3487
3699
  init_health();
@@ -4112,7 +4324,7 @@ var init_server2 = __esm({
4112
4324
  "server/src/mcp/server.ts"() {
4113
4325
  "use strict";
4114
4326
  init_protocol();
4115
- VERSION = true ? "0.14.4" : "dev";
4327
+ VERSION = true ? "0.15.0" : "dev";
4116
4328
  }
4117
4329
  });
4118
4330
 
@@ -7492,7 +7704,7 @@ var program = new Command();
7492
7704
  // server/src/cli/program.ts
7493
7705
  init_paths();
7494
7706
  init_settings();
7495
- var VERSION2 = true ? "0.14.4" : "dev";
7707
+ var VERSION2 = true ? "0.15.0" : "dev";
7496
7708
  async function bootstrap() {
7497
7709
  const file = configFilePath();
7498
7710
  const settings = readSettings(file);