@tea-agent/loop-agent 0.44.0-next.6 → 0.44.0-next.7

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 (92) hide show
  1. package/CHANGELOG.md +285 -454
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/shared/git-workspace-status.js +2 -0
  4. package/dist/worker/console/chat/assistant-content.js +5 -34
  5. package/dist/worker/console/chat/chat-event-store.js +27 -10
  6. package/dist/worker/console/chat/db-connections.js +72 -8
  7. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
  8. package/dist/worker/console/chat/pi-runtime.js +34 -0
  9. package/dist/worker/console/chat/routes.js +138 -9
  10. package/dist/worker/console/chat/session-store.js +147 -19
  11. package/dist/worker/console/chat/turn-process.js +69 -23
  12. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  13. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Brn2sPzL.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
  14. package/dist/worker/console/static/assets/{arc-BIMNTG0W.js → arc-CQbCsSqb.js} +1 -1
  15. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G--WtcgR_A.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
  16. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BT_Ztog1.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
  17. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5hbVr-l.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
  18. package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
  19. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BKKnyR9Z.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BHs6biqA.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BdSFy_vP.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpJIdTKV.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C_iWJhkT.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BTbmx3-k.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D4Ee6DmP.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B487SORR.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BKYHjNrr.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DrTdoYZG.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
  29. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
  30. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
  31. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CttUY_Jo.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
  32. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B4Rm5gnR.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
  33. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-ByxHBq_S.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
  34. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DGrfNFA0.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-KjrUPquv.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-VS4mlm75.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BLXYmFTM.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C6xoFAqH.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C478dCe7.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
  40. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DHEpW5zN.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
  41. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CA5GKfo9.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
  42. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-zysi0Fl-.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
  43. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGzcKA8Y.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
  44. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CHsuIG1w.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
  45. package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
  46. package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
  47. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BexZHPCp.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
  48. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
  49. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-XDW3qrJ7.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
  50. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ07eEaN.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
  51. package/dist/worker/console/static/assets/{linear-DL-zeqOn.js → linear-BGKgMc2T.js} +1 -1
  52. package/dist/worker/console/static/assets/{mermaid.core-C0HdXtOf.js → mermaid.core-DF6mMPJ9.js} +5 -5
  53. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CqRX0lrt.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
  54. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVM9JZ2H.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
  55. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BA3ozdKk.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
  56. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B-0_p9_4.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
  57. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DHRzPWFc.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
  58. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-9BEC_PcW.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
  59. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DMxeW4rN.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
  60. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DixP6LIU.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
  61. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B_EifJBo.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
  62. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BwikwWbX.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
  63. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
  64. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C162l9o1.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
  65. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
  66. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-mpe_u1i4.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
  67. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DyUk54HX.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
  68. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-HYyMkzfm.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
  69. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DRoh7skR.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
  70. package/dist/worker/console/static/index.html +2 -2
  71. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  72. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
  73. package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
  74. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  75. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
  76. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  77. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  78. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  79. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -43
  80. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
  81. package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
  82. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +5 -2
  83. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  84. package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
  85. package/package.json +1 -1
  86. package/dist/worker/console/static/assets/channel-DNWB4hpR.js +0 -1
  87. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +0 -1
  88. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +0 -1
  89. package/dist/worker/console/static/assets/index-CJh6pVLm.js +0 -482
  90. package/dist/worker/console/static/assets/index-C_kvKmFM.css +0 -1
  91. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +0 -1
  92. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +0 -8
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.44.0-next.6",
4
- "gitSha": "11c9ac87f792e61b1bc31b1ef2aeacb48a6334d4",
5
- "builtAt": "2026-09-16T11:49:07.558Z"
3
+ "version": "0.44.0-next.7",
4
+ "gitSha": "561015cd071b2aba74c823716703e756d2236929",
5
+ "builtAt": "2026-09-16T18:31:45.598Z"
6
6
  }
@@ -133,6 +133,8 @@ export function unquoteGitPath(value) {
133
133
  * shell-quoting hazards). Returns [] when the file is absent or empty.
134
134
  */
135
135
  export async function readSubmoduleRegistrations(rootCwd, dependencies = DEFAULT_WORKSPACE_STATUS_DEPENDENCIES) {
136
+ if (!(await hasGitmodulesFile(rootCwd)))
137
+ return [];
136
138
  const result = await dependencies.runGit(rootCwd, [
137
139
  "config",
138
140
  "-f",
@@ -102,43 +102,14 @@ export function splitFinalAssistantBlocks(blocks, options = {}) {
102
102
  // after text that is already visible. Keep the whole live message in the
103
103
  // chronological process subtree so that update appends the tool in place
104
104
  // instead of reparenting the earlier text from answer -> process.
105
- if (options.isStreaming) {
106
- return { answerBlocks: [], processBlocks: displayable };
107
- }
108
- let lastProcessIndex = -1;
109
- for (let i = displayable.length - 1; i >= 0; i--) {
110
- if (!isFinalAnswerBlock(displayable[i])) {
111
- lastProcessIndex = i;
112
- break;
113
- }
114
- }
115
- if (lastProcessIndex === -1) {
116
- return { answerBlocks: displayable, processBlocks: [] };
117
- }
118
- const trailing = displayable.slice(lastProcessIndex + 1);
119
- if (trailing.length > 0) {
120
- return {
121
- answerBlocks: trailing,
122
- processBlocks: displayable.slice(0, lastProcessIndex + 1),
123
- };
124
- }
125
- // The stage ends on a non-text block (a tool call after the prose, or a final
126
- // thinking block). Returning an empty answer here hid the whole reply inside
127
- // the collapsed process subtree; keep the last prose block as the visible
128
- // answer instead (2026-09-08).
129
- let lastTextIndex = -1;
130
- for (let i = displayable.length - 1; i >= 0; i--) {
131
- if (isFinalAnswerBlock(displayable[i])) {
132
- lastTextIndex = i;
133
- break;
134
- }
135
- }
136
- if (lastTextIndex === -1) {
105
+ if (options.isStreaming ||
106
+ displayable.some((block) => block.type === "toolCall")) {
137
107
  return { answerBlocks: [], processBlocks: displayable };
138
108
  }
109
+ const answerBlocks = displayable.filter((block) => isFinalAnswerBlock(block) && block.text.trim() !== "");
139
110
  return {
140
- answerBlocks: [displayable[lastTextIndex]],
141
- processBlocks: displayable.filter((_block, index) => index !== lastTextIndex),
111
+ answerBlocks,
112
+ processBlocks: displayable.filter((block) => !isFinalAnswerBlock(block)),
142
113
  };
143
114
  }
144
115
  export function joinAssistantText(blocks) {
@@ -334,6 +334,18 @@ export function projectChatToolEventPayload(input) {
334
334
  };
335
335
  }
336
336
  const { preview, previewHash } = projectToolResultPreview(input.value);
337
+ if (input.kind === "tool_update") {
338
+ return {
339
+ toolCallId: input.toolCallId,
340
+ toolName: input.toolName,
341
+ resultPreview: preview,
342
+ previewHash,
343
+ ...(input.step === undefined ? {} : { step: input.step }),
344
+ ...(input.parentToolCallId
345
+ ? { parentToolCallId: input.parentToolCallId }
346
+ : {}),
347
+ };
348
+ }
337
349
  return {
338
350
  toolCallId: input.toolCallId,
339
351
  toolName: input.toolName,
@@ -438,22 +450,27 @@ function dropOneByPriority(ring) {
438
450
  return events.shift();
439
451
  }
440
452
  /**
441
- * D2 coalescing decision: merge an incoming `message_update` into the ring tail
442
- * only when the tail is also a same-turn `message_update` and the update landed
443
- * inside the coalescing window. Structural events close the window implicitly
444
- * (their presence makes the tail check fail), so an
445
- * update → tool_call → update sequence never merges across the tool boundary.
453
+ * D2 coalescing decision: merge high-frequency cumulative text/tool updates
454
+ * into a matching ring tail inside the coalescing window. Structural events
455
+ * close the window implicitly (their presence makes the tail check fail).
446
456
  * `partial.at` (when present) drives the clock so tests can inject window
447
457
  * boundaries deterministically without fake timers.
448
458
  */
449
459
  function shouldCoalesce(ring, turnId, partial) {
450
- if (partial.kind !== "message_update")
451
- return false;
452
460
  const tail = ring.events.at(-1);
453
- if (!tail || tail.kind !== "message_update")
461
+ if (!tail || tail.kind !== partial.kind)
454
462
  return false;
455
463
  if (tail.turnId !== turnId)
456
464
  return false;
465
+ if (partial.kind === "tool_update") {
466
+ const currentId = partial.data.toolCallId;
467
+ const tailId = tail.data.toolCallId;
468
+ if (!currentId || currentId !== tailId)
469
+ return false;
470
+ }
471
+ else if (partial.kind !== "message_update") {
472
+ return false;
473
+ }
457
474
  const nowMs = partial.at ? Date.parse(partial.at) : Date.now();
458
475
  if (Number.isNaN(nowMs))
459
476
  return false;
@@ -1048,12 +1065,12 @@ export function createChatEventStore(options) {
1048
1065
  ring.approxBytes += byteHint(live);
1049
1066
  }
1050
1067
  enforceBounds(sessionId, ring);
1051
- // B5-1: only high-frequency `message_update` appends defer the persist;
1068
+ // B5-1: high-frequency cumulative updates defer the persist;
1052
1069
  // every other kind (message_end / tool_call / error / turn signals …)
1053
1070
  // still persists synchronously. Because persistNow writes the FULL
1054
1071
  // in-memory ring, one flush also lands any earlier pending updates in
1055
1072
  // order — no two-phase flush is needed.
1056
- if (partial.kind === "message_update") {
1073
+ if (partial.kind === "message_update" || partial.kind === "tool_update") {
1057
1074
  const atMs = partial.at ? Date.parse(partial.at) : Date.now();
1058
1075
  if (!Number.isNaN(atMs)) {
1059
1076
  persistDebounced(sessionId, ring, atMs);
@@ -525,7 +525,6 @@ export class DbConnectionRegistry {
525
525
  async listTables(name, selectedDatabase) {
526
526
  const config = this.getConfig(name);
527
527
  const effectiveDatabase = selectedDatabase?.trim() || config.database;
528
- const handle = await this.connect(name, effectiveDatabase);
529
528
  const sql = config.engine === "sqlite"
530
529
  ? "SELECT name, type FROM sqlite_master WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name"
531
530
  : config.engine === "postgres"
@@ -533,7 +532,7 @@ export class DbConnectionRegistry {
533
532
  : effectiveDatabase
534
533
  ? "SELECT table_name AS name, CASE WHEN table_type = 'VIEW' THEN 'view' ELSE 'table' END AS type, table_schema AS schemaName FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name"
535
534
  : "SELECT table_name AS name, CASE WHEN table_type = 'VIEW' THEN 'view' ELSE 'table' END AS type, table_schema AS schemaName FROM information_schema.tables WHERE table_schema NOT IN ('information_schema','mysql','performance_schema','sys') ORDER BY table_schema, table_name";
536
- const rows = await handle.query(sql);
535
+ const rows = await this.queryWithReconnect(name, (handle) => handle.query(sql), effectiveDatabase);
537
536
  return rows.map((row) => ({
538
537
  name: String(row.name ?? ""),
539
538
  type: String(row.type).toLowerCase() === "view" ? "view" : "table",
@@ -556,8 +555,7 @@ export class DbConnectionRegistry {
556
555
  ? `\`${table.schema.replaceAll("\`", "\`\`")}\`.${escapedName}`
557
556
  : escapedName;
558
557
  const columns = await this.describeTable(config, table.name, identifier, table.schema, selectedDatabase);
559
- const handle = await this.connect(name, selectedDatabase);
560
- const rows = await handle.query(`SELECT * FROM ${identifier} LIMIT ${safePageSize + 1} OFFSET ${offset}`);
558
+ const rows = await this.queryWithReconnect(name, (handle) => handle.query(`SELECT * FROM ${identifier} LIMIT ${safePageSize + 1} OFFSET ${offset}`), selectedDatabase);
561
559
  return {
562
560
  connection: name,
563
561
  table: requestedTable,
@@ -576,19 +574,18 @@ export class DbConnectionRegistry {
576
574
  return config;
577
575
  }
578
576
  async describeTable(config, table, identifier, schema, selectedDatabase) {
579
- const handle = await this.connect(config.name, selectedDatabase);
580
577
  if (config.engine === "sqlite") {
581
- const rows = await handle.query(`PRAGMA table_info(${identifier})`);
578
+ const rows = await this.queryWithReconnect(config.name, (h) => h.query(`PRAGMA table_info(${identifier})`), selectedDatabase);
582
579
  return rows.map((row) => ({ name: String(row.name), type: String(row.type ?? ""), nullable: Number(row.notnull ?? 0) !== 1, primaryKey: Number(row.pk ?? 0) > 0 }));
583
580
  }
584
581
  if (config.engine === "postgres") {
585
582
  const literal = table.replaceAll("'", "''");
586
- const rows = await handle.query(`SELECT column_name AS name, data_type AS type, is_nullable AS nullable FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = '${literal}' ORDER BY ordinal_position`);
583
+ const rows = await this.queryWithReconnect(config.name, (h) => h.query(`SELECT column_name AS name, data_type AS type, is_nullable AS nullable FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = '${literal}' ORDER BY ordinal_position`), selectedDatabase);
587
584
  return rows.map((row) => ({ name: String(row.name), type: String(row.type ?? ""), nullable: String(row.nullable).toUpperCase() === "YES", primaryKey: false }));
588
585
  }
589
586
  const literal = table.replaceAll("'", "''");
590
587
  const schemaFilter = schema ? `'${schema.replaceAll("'", "''")}'` : "DATABASE()";
591
- const rows = await handle.query(`SELECT column_name AS name, column_type AS type, is_nullable AS nullable, column_key AS columnKey FROM information_schema.columns WHERE table_schema = ${schemaFilter} AND table_name = '${literal}' ORDER BY ordinal_position`);
588
+ const rows = await this.queryWithReconnect(config.name, (h) => h.query(`SELECT column_name AS name, column_type AS type, is_nullable AS nullable, column_key AS columnKey FROM information_schema.columns WHERE table_schema = ${schemaFilter} AND table_name = '${literal}' ORDER BY ordinal_position`), selectedDatabase);
592
589
  return rows.map((row) => ({ name: String(row.name), type: String(row.type ?? ""), nullable: String(row.nullable).toUpperCase() === "YES", primaryKey: String(row.columnKey).toUpperCase() === "PRI" }));
593
590
  }
594
591
  async connect(name, databaseOverride) {
@@ -605,6 +602,42 @@ export class DbConnectionRegistry {
605
602
  this.handles.set(name, handle);
606
603
  return handle;
607
604
  }
605
+ /**
606
+ * Public read-only query entry point with transparent one-shot reconnect.
607
+ * Tools should prefer this over raw connect()+query so idle-killed
608
+ * sessions heal without operator intervention.
609
+ */
610
+ async query(name, sql) {
611
+ return this.queryWithReconnect(name, (handle) => handle.query(sql));
612
+ }
613
+ /**
614
+ * Drop the cached connection for `name` (if any) so the next connect()
615
+ * reopens it. Callers that hold a raw handle across queries can use this
616
+ * after a connection-level failure to force a fresh session.
617
+ */
618
+ evict(name) {
619
+ this.closeHandle(name);
620
+ }
621
+ /**
622
+ * Query through the cached connection for `name`, transparently reopening it
623
+ * once when the server has dropped the idle session (MySQL wait_timeout,
624
+ * TCP reset, pool death). Reopen uses the exact same config, so credential
625
+ * rules are unchanged; only connection-level failures trigger the retry.
626
+ */
627
+ async queryWithReconnect(name, run, databaseOverride) {
628
+ const handle = await this.connect(name, databaseOverride);
629
+ try {
630
+ return await run(handle);
631
+ }
632
+ catch (error) {
633
+ if (!isConnectionLossError(error))
634
+ throw error;
635
+ // Evict the dead cached handle and reopen once with the same config.
636
+ this.closeHandle(name);
637
+ const reopened = await this.connect(name, databaseOverride);
638
+ return run(reopened);
639
+ }
640
+ }
608
641
  async openHandle(config, databaseOverride) {
609
642
  if (config.engine === "sqlite") {
610
643
  const { DatabaseSync } = await import("node:sqlite");
@@ -727,6 +760,28 @@ function decryptCredentialDocument(serialized, key) {
727
760
  throw new Error("invalid encrypted credential entries");
728
761
  return document;
729
762
  }
763
+ /** Errors that mean the underlying server session is gone and a reopen may heal it. */
764
+ function isConnectionLossError(error) {
765
+ if (!error || typeof error !== "object")
766
+ return false;
767
+ const err = error;
768
+ const code = typeof err.code === "string" ? err.code.toUpperCase() : "";
769
+ const message = typeof err.message === "string" ? err.message : "";
770
+ if (code === "ECONNRESET" ||
771
+ code === "EPIPE" ||
772
+ code === "ETIMEDOUT" ||
773
+ code === "ECONNREFUSED" ||
774
+ code === "PROTOCOL_CONNECTION_LOST" ||
775
+ code === "CONNECTION_LOST" ||
776
+ code === "57P01" || // postgres: admin_shutdown
777
+ code === "57P03" || // postgres: cannot_connect_now
778
+ code === "08003" || // SQLSTATE connection_does_not_exist
779
+ code === "08006" // SQLSTATE connection_failure
780
+ ) {
781
+ return true;
782
+ }
783
+ return /connection lost|connection closed|connection ended|connection terminated|server closed the connection unexpectedly/i.test(message);
784
+ }
730
785
  /** PostgreSQL driver adapter (`pg`). */
731
786
  async function connectPostgres(driver, options) {
732
787
  const mod = driver;
@@ -770,6 +825,15 @@ async function connectMysql(driver, options) {
770
825
  password: options.password,
771
826
  ...(options.ssl ? { ssl: {} } : {}),
772
827
  });
828
+ // mysql2 emits 'error' asynchronously when the server kills an idle
829
+ // session; without a listener that becomes an unhandled 'error' event
830
+ // that can take the whole Console process down.
831
+ if (typeof conn.on === "function") {
832
+ conn.on("error", () => {
833
+ // Swallow: the query wrapper surfaces real failures and the
834
+ // registry reconnect logic reopens the connection on demand.
835
+ });
836
+ }
773
837
  return {
774
838
  async query(sql) {
775
839
  assertDatabaseReadOnlySql(sql);
@@ -309,6 +309,28 @@ export async function buildSqlTools(options) {
309
309
  throw new SqlToolError("DB_CONFIRMATION_REQUIRED", `the user rejected the confirmation for "${id}"`);
310
310
  }
311
311
  }
312
+ /** Run a registry operation, retrying exactly once when the underlying DB session died. */
313
+ async function withDbReconnect(registry, connection, op) {
314
+ try {
315
+ return await op();
316
+ }
317
+ catch (error) {
318
+ if (!isDbConnectionLoss(error))
319
+ throw error;
320
+ registry.evict(connection);
321
+ return op();
322
+ }
323
+ }
324
+ function isDbConnectionLoss(error) {
325
+ if (!error || typeof error !== "object")
326
+ return false;
327
+ const err = error;
328
+ const code = typeof err.code === "string" ? err.code.toUpperCase() : "";
329
+ const message = typeof err.message === "string" ? err.message : "";
330
+ if (["ECONNRESET", "EPIPE", "ETIMEDOUT", "PROTOCOL_CONNECTION_LOST", "CONNECTION_LOST", "57P01", "57P03", "08003", "08006"].includes(code))
331
+ return true;
332
+ return /connection lost|connection closed|connection ended|connection terminated|server closed the connection unexpectedly/i.test(message);
333
+ }
312
334
  async function envelope(operation, timeoutMs) {
313
335
  let result;
314
336
  try {
@@ -378,21 +400,25 @@ export async function buildSqlTools(options) {
378
400
  }
379
401
  assertSingleStatement(statements);
380
402
  assertReadOnlyStatement(statements[0]);
381
- const handle = await registry.connect(params.connection);
382
403
  const limit = Math.min(Math.max(params.maxRows ?? SQL_MAX_ROWS_DEFAULT, 1), SQL_MAX_ROWS_LIMIT);
383
404
  // sqlite exposes column metadata via prepare(); pg/mysql derive it
384
- // from the first result row.
385
- let columns;
386
- let all;
387
- if (isSqliteHandle(handle)) {
388
- const stmt = handle.prepare(params.sql.trim());
389
- columns = stmt.columns().map((c) => c.name);
390
- all = stmt.all();
391
- }
392
- else {
393
- all = await handle.query(params.sql.trim());
394
- columns = all.length > 0 ? Object.keys(all[0]) : [];
395
- }
405
+ // from the first result row. The whole fetch retries once on a
406
+ // dead session (idle timeout) via withDbReconnect.
407
+ const { columns, all } = await withDbReconnect(registry, params.connection, async () => {
408
+ const handle = await registry.connect(params.connection);
409
+ if (isSqliteHandle(handle)) {
410
+ const stmt = handle.prepare(params.sql.trim());
411
+ return {
412
+ columns: stmt.columns().map((c) => c.name),
413
+ all: stmt.all(),
414
+ };
415
+ }
416
+ const rows = await handle.query(params.sql.trim());
417
+ return {
418
+ columns: rows.length > 0 ? Object.keys(rows[0]) : [],
419
+ all: rows,
420
+ };
421
+ });
396
422
  const truncated = all.length > limit;
397
423
  const rows = truncated ? all.slice(0, limit) : all;
398
424
  const format = params.format ?? "json";
@@ -445,13 +471,12 @@ export async function buildSqlTools(options) {
445
471
  }, { additionalProperties: false }),
446
472
  async execute(_toolCallId, params) {
447
473
  return envelope(async () => {
448
- const handle = await registry.connect(params.connection);
449
474
  if (params.table === undefined || params.table.trim() === "") {
450
- const rows = (await handle.query("SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"));
475
+ const rows = await withDbReconnect(registry, params.connection, async () => (await registry.connect(params.connection)).query("SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"));
451
476
  return { ok: true, connection: params.connection, tables: rows };
452
477
  }
453
478
  const table = assertSafeIdentifier(params.table);
454
- const rows = await handle.query(`PRAGMA table_info(${table})`);
479
+ const rows = await withDbReconnect(registry, params.connection, async () => (await registry.connect(params.connection)).query(`PRAGMA table_info(${table})`));
455
480
  return {
456
481
  ok: true,
457
482
  connection: params.connection,
@@ -474,14 +499,17 @@ export async function buildSqlTools(options) {
474
499
  parameters: Type.Object({ connection: connectionParameter }, { additionalProperties: false }),
475
500
  async execute(_toolCallId, params) {
476
501
  return envelope(async () => {
477
- const handle = await registry.connect(params.connection);
478
- const tables = (await handle.query("SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"));
479
- const rowsByTable = {};
480
- for (const t of tables) {
481
- const name = assertSafeIdentifier(t.name);
482
- const countRows = await handle.query(`SELECT COUNT(*) AS n FROM ${name}`);
483
- rowsByTable[name] = Number(countRows[0]?.n ?? 0);
484
- }
502
+ const { tables, rowsByTable } = await withDbReconnect(registry, params.connection, async () => {
503
+ const handle = await registry.connect(params.connection);
504
+ const tables = (await handle.query("SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"));
505
+ const rowsByTable = {};
506
+ for (const t of tables) {
507
+ const name = assertSafeIdentifier(t.name);
508
+ const countRows = await handle.query(`SELECT COUNT(*) AS n FROM ${name}`);
509
+ rowsByTable[name] = Number(countRows[0]?.n ?? 0);
510
+ }
511
+ return { tables, rowsByTable };
512
+ });
485
513
  const config = registry.get(params.connection);
486
514
  let sizeBytes;
487
515
  if (config?.path) {
@@ -191,6 +191,25 @@ async function bindSessionExtensions(session) {
191
191
  return;
192
192
  await session.bindExtensions(createConsoleExtensionBindings());
193
193
  }
194
+ export function branchForkedSessionManager(sessionManager, throughUserMessageIndex) {
195
+ const userEntries = (sessionManager.getBranch?.() ?? []).filter((entry) => {
196
+ if (!entry || typeof entry !== "object")
197
+ return false;
198
+ const candidate = entry;
199
+ return candidate.type === "message" && candidate.message?.role === "user";
200
+ });
201
+ const target = userEntries[throughUserMessageIndex];
202
+ if (!target || typeof target.id !== "string") {
203
+ throw new Error("FORK_MESSAGE_ENTRY_NOT_FOUND");
204
+ }
205
+ sessionManager.branch(target.id);
206
+ if (typeof sessionManager.appendCustomEntry !== "function") {
207
+ throw new Error("FORK_MESSAGE_ENTRY_PERSIST_UNSUPPORTED");
208
+ }
209
+ sessionManager.appendCustomEntry("loop-agent-fork-point", {
210
+ throughUserMessageIndex,
211
+ });
212
+ }
194
213
  function pinOperatorChatTools(session, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
195
214
  session.setActiveToolsByName(computeChatActiveToolNames(session, sessionMode));
196
215
  }
@@ -1198,6 +1217,9 @@ export class ConsolePiRuntime {
1198
1217
  targetSessionDir: this.options.sessionDir,
1199
1218
  targetSessionId: sessionId,
1200
1219
  });
1220
+ if (init.throughUserMessageIndex !== undefined) {
1221
+ branchForkedSessionManager(sessionManager, init.throughUserMessageIndex);
1222
+ }
1201
1223
  const built = await this.buildSessionWithServices({
1202
1224
  sessionId,
1203
1225
  sessionManager,
@@ -2692,6 +2714,18 @@ export function mapSdkEvent(sessionId, event, compactionId, step = 1) {
2692
2714
  ? { parentToolCallId: event.parentToolCallId }
2693
2715
  : {}),
2694
2716
  };
2717
+ case "tool_execution_update":
2718
+ return {
2719
+ type: "tool_update",
2720
+ sessionId,
2721
+ toolCallId: event.toolCallId,
2722
+ toolName: event.toolName,
2723
+ partialResult: event.partialResult,
2724
+ step,
2725
+ ...(event.parentToolCallId
2726
+ ? { parentToolCallId: event.parentToolCallId }
2727
+ : {}),
2728
+ };
2695
2729
  case "tool_execution_end":
2696
2730
  return {
2697
2731
  type: "tool_result",