@tea-agent/loop-agent 0.44.0-next.5 → 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 (111) hide show
  1. package/CHANGELOG.md +299 -445
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +7 -7
  4. package/dist/infrastructure/console/operation-store.js +46 -14
  5. package/dist/shared/git-workspace-status.js +2 -0
  6. package/dist/shared/wizard-local-telemetry.js +2 -1
  7. package/dist/worker/console/chat/assistant-content.js +5 -34
  8. package/dist/worker/console/chat/chat-event-store.js +27 -10
  9. package/dist/worker/console/chat/db-connections.js +72 -8
  10. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
  11. package/dist/worker/console/chat/pi-runtime.js +251 -18
  12. package/dist/worker/console/chat/repo-browser.js +49 -0
  13. package/dist/worker/console/chat/routes.js +215 -66
  14. package/dist/worker/console/chat/session-store.js +147 -19
  15. package/dist/worker/console/chat/turn-process.js +69 -23
  16. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  17. package/dist/worker/console/console-update-runtime.js +2 -2
  18. package/dist/worker/console/operator-user-error.js +28 -1
  19. package/dist/worker/console/pi-readiness.js +4 -4
  20. package/dist/worker/console/recovery-cta.js +14 -14
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BHuhGM1e.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-CKh5E_sr.js → arc-CQbCsSqb.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DizhiBEv.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-3Neea-Z-.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCP_Qrgi.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C_wydftK.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-jRPXZbzx.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BLQf2OYO.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-0qRq64rI.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-ClMg8xmV.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Coop2FlL.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CDdO-jPJ.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D8Gkppjl.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DA9HIySf.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DZNDRX1k.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CZ1zGyjq.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B8u1_FtM.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CPBB6lfo.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CG3Eov3E.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ycI-dOjh.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bx_GyQ7Y.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-m0G6mP4d.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DTDJkqcJ.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BCfFrG-p.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BIkTNPpp.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DTcMWrd2.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqzDaNQ7.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSl_vGFe.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCA5HXM6.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
  53. package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
  54. package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DvqT18ge.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BpLQPI9C.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CzIz_-Lt.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-CMQ8Jpp-.js → linear-BGKgMc2T.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-BdAMrTf7.js → mermaid.core-DF6mMPJ9.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-hSwF6sOq.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DZdv509B.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CmD4-b0K.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKxNxlS.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-D48iIoiS.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3N0K-2p.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CACWR-Xs.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-g_QgugvZ.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqLoRPGK.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-ES59VgMG.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-fjyLVNWo.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cd5-biKv.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CZTu6mPf.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DR01jjbe.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-iZcuGVGa.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +9 -1
  80. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  81. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
  82. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  83. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  84. package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
  85. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  86. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
  87. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  88. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  89. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  90. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -41
  91. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
  92. package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
  93. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  95. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  96. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  97. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  98. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  99. package/dist/worker/console/workspace-initialization.js +3 -3
  100. package/dist/worker/observability/read-model.js +23 -2
  101. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  102. package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
  103. package/docs/templates/backend-test-dag.json +1 -0
  104. package/package.json +4 -4
  105. package/dist/worker/console/static/assets/channel-DxgzA1GF.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +0 -1
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +0 -1
  108. package/dist/worker/console/static/assets/index-BAEDjTbD.css +0 -1
  109. package/dist/worker/console/static/assets/index-wKEJUaP5.js +0 -482
  110. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +0 -1
  111. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +0 -8
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.44.0-next.5",
4
- "gitSha": "da4e19a774406e1fe79611807fd241e56e6e4c17",
5
- "builtAt": "2026-09-16T02:11:12.294Z"
3
+ "version": "0.44.0-next.7",
4
+ "gitSha": "561015cd071b2aba74c823716703e756d2236929",
5
+ "builtAt": "2026-09-16T18:31:45.598Z"
6
6
  }
@@ -40,21 +40,21 @@ export async function installPiProviderRequestBudget(session, reserve, onFailure
40
40
  const agent = session.agent;
41
41
  const original = agent?.streamFunction;
42
42
  if (!agent || typeof original !== "function")
43
- throw Error("FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: SDK actual request stream unavailable");
43
+ throw new Error("FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: SDK actual request stream unavailable");
44
44
  const { createAssistantMessageEventStream } = await import("@earendil-works/pi-ai");
45
45
  agent.streamFunction = async (...args) => {
46
46
  try {
47
47
  if (args[2]?.signal?.aborted)
48
- throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
48
+ throw new Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
49
49
  if (capacity) {
50
50
  const measurement = measureFrontendRequestEnvelope(args[1], args[0], capacity.policy);
51
51
  capacity.observe(measurement);
52
52
  if (measurement.exceedsEstimate)
53
- throw Error(`FRONTEND_CONTEXT_ESTIMATE_EXCEEDED: estimated=${measurement.estimatedTokens} available=${measurement.availableInputTokens}; estimator=${measurement.estimatorVersion}; registry capability is not a measured gateway limit`);
53
+ throw new Error(`FRONTEND_CONTEXT_ESTIMATE_EXCEEDED: estimated=${measurement.estimatedTokens} available=${measurement.availableInputTokens}; estimator=${measurement.estimatorVersion}; registry capability is not a measured gateway limit`);
54
54
  }
55
55
  await reserve();
56
56
  if (args[2]?.signal?.aborted)
57
- throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
57
+ throw new Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
58
58
  }
59
59
  catch (error) {
60
60
  const message = error instanceof Error ? error.message : String(error);
@@ -124,7 +124,7 @@ export function setPiSdkImportOverrideForTests(fn) {
124
124
  export function setPiSdkModuleOverrideForTests(fn) {
125
125
  sdkModuleOverrideForTests = fn;
126
126
  }
127
- /** Check whether the Pi SDK runtime dependency satisfies the 0.83.0 runtime contract. */
127
+ /** Check whether the Pi SDK runtime dependency satisfies the 0.84.4 runtime contract. */
128
128
  export async function checkPiSdkAvailability(_repoRoot) {
129
129
  if (sdkSessionFactoryOverride) {
130
130
  return { ok: true, detail: "pi SDK session factory override active" };
@@ -140,10 +140,10 @@ export async function checkPiSdkAvailability(_repoRoot) {
140
140
  typeof ModelRuntime?.create !== "function") {
141
141
  return {
142
142
  ok: false,
143
- detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.83.0 contract)",
143
+ detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.84.4 contract)",
144
144
  };
145
145
  }
146
- return { ok: true, detail: "pi SDK 0.83.0 contract available" };
146
+ return { ok: true, detail: "pi SDK 0.84.4 contract available" };
147
147
  }
148
148
  catch (error) {
149
149
  const message = error instanceof Error ? error.message : String(error);
@@ -6,6 +6,24 @@ import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finali
6
6
  import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
7
7
  import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
8
8
  export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
9
+ /**
10
+ * Operation states that hold an admission slot. needs-reconcile requires
11
+ * inspection but does not itself prove a live operation; callers still check
12
+ * DAG/Worker/chat activity separately. restarting is the terminal receipt of
13
+ * a successful Console handoff, retained across boots, not a live slot.
14
+ */
15
+ export const OCCUPIED_OPERATION_STATES = new Set(["queued", "starting", "running"]);
16
+ /** True when the state can still make progress and therefore holds the slot. */
17
+ export function isOperationOccupied(state) {
18
+ return OCCUPIED_OPERATION_STATES.has(state);
19
+ }
20
+ /**
21
+ * True when no further state transition will ever occur: a business terminal
22
+ * (succeeded/failed/timed-out/restarting) or a stranded needs-reconcile.
23
+ */
24
+ export function isOperationSettled(state) {
25
+ return TERMINAL_OPERATION_STATES.has(state) || state === "needs-reconcile";
26
+ }
9
27
  function operationFile(appData, operationId) {
10
28
  return path.join(appData.operations, `${operationId}.json`);
11
29
  }
@@ -359,14 +377,11 @@ export class OperationStore {
359
377
  for (const op of await this.list()) {
360
378
  if (TERMINAL_OPERATION_STATES.has(op.state))
361
379
  continue;
362
- // A consoleUpdateAndInit whose record already proves a requested
363
- // handoff died before writing its terminal state: the update and
364
- // restart themselves succeeded — the booting process IS the
365
- // handoff target. Mark it succeeded instead of surfacing the
366
- // generic needs-reconcile failure copy to the operator.
380
+ // A consoleUpdateAndInit that proved its handoff before the process
381
+ // died must not be reported as a stranded failure: the booting
382
+ // process IS the handoff target, so the update actually succeeded.
367
383
  if (op.action === "consoleUpdateAndInit" &&
368
- op.state === "needs-reconcile" &&
369
- isProvenConsoleUpdateHandoff(op.result)) {
384
+ isProvenConsoleUpdateHandoff(op)) {
370
385
  changed.push(await this.update(op.operationId, {
371
386
  state: "succeeded",
372
387
  errorCode: undefined,
@@ -398,17 +413,34 @@ export function isTerminalOperationState(state) {
398
413
  return TERMINAL_OPERATION_STATES.has(state);
399
414
  }
400
415
  /**
401
- * True when a consoleUpdateAndInit result proves the handoff was requested
402
- * (install verified + init clean + new Console spawned). Used by boot
403
- * reconcile to close the record as succeeded even though the old process
404
- * died before writing its terminal state.
416
+ * True when a consoleUpdateAndInit record proves the handoff actually
417
+ * succeeded (install verified + new Console spawned), so boot reconcile can
418
+ * close it as succeeded even though the dying process never wrote its
419
+ * terminal state.
420
+ *
421
+ * The proof reads the maintenance projection rather than only `result.state`.
422
+ * The old check required `result.state === "restarting"`, but the executor
423
+ * writes `state` and `maintenance` together (`console-update-and-init.ts`),
424
+ * so any path that recorded the handoff proof together with a non-restarting
425
+ * `state` — most importantly the `needs-reconcile` record that
426
+ * `acceptConsoleUpdateAndInit` writes when the executor throws *after* a
427
+ * successful handoff — could never satisfy it. Such records were stranded
428
+ * forever: boot reconcile skipped them and they blocked later updates.
429
+ *
430
+ * `handoff.status === "requested"` is only ever set on the success path, and
431
+ * `package.status === "failed"` explicitly disqualifies a record, so this
432
+ * stays fail-closed for genuine failures.
405
433
  */
406
- function isProvenConsoleUpdateHandoff(result) {
434
+ export function isProvenConsoleUpdateHandoff(operation) {
435
+ if (!operation || operation.action !== "consoleUpdateAndInit")
436
+ return false;
437
+ const result = operation.result;
407
438
  if (!result || typeof result !== "object")
408
439
  return false;
409
440
  const shape = result;
410
- return (shape.state === "restarting" &&
411
- shape.maintenance?.handoff?.status === "requested");
441
+ if (shape.maintenance?.handoff?.status !== "requested")
442
+ return false;
443
+ return shape.maintenance?.package?.status !== "failed";
412
444
  }
413
445
  export function clipPreview(text, max = 4096) {
414
446
  if (text.length <= max)
@@ -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",
@@ -12,7 +12,8 @@ function deleteHeaderCaseInsensitive(headers, name) {
12
12
  }
13
13
  function configuredHeader(headers, name) {
14
14
  const key = Object.keys(headers ?? {}).find((entry) => entry.toLowerCase() === name.toLowerCase());
15
- return key ? headers?.[key] : undefined;
15
+ const value = key ? headers?.[key] : undefined;
16
+ return typeof value === "string" ? value : undefined;
16
17
  }
17
18
  function setStaticHeader(headers, configured, name, defaultValue) {
18
19
  deleteHeaderCaseInsensitive(headers, name);
@@ -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) {