@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.
- package/CHANGELOG.md +299 -445
- package/dist/build-stamp.json +3 -3
- package/dist/executors/pi-sdk-executor.js +7 -7
- package/dist/infrastructure/console/operation-store.js +46 -14
- package/dist/shared/git-workspace-status.js +2 -0
- package/dist/shared/wizard-local-telemetry.js +2 -1
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +27 -10
- package/dist/worker/console/chat/db-connections.js +72 -8
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
- package/dist/worker/console/chat/pi-runtime.js +251 -18
- package/dist/worker/console/chat/repo-browser.js +49 -0
- package/dist/worker/console/chat/routes.js +215 -66
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/turn-process.js +69 -23
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BHuhGM1e.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CKh5E_sr.js → arc-CQbCsSqb.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DizhiBEv.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-3Neea-Z-.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCP_Qrgi.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C_wydftK.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-jRPXZbzx.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BLQf2OYO.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-0qRq64rI.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-ClMg8xmV.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Coop2FlL.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CDdO-jPJ.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D8Gkppjl.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DA9HIySf.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DZNDRX1k.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CZ1zGyjq.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B8u1_FtM.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CPBB6lfo.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CG3Eov3E.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ycI-dOjh.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bx_GyQ7Y.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-m0G6mP4d.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DTDJkqcJ.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BCfFrG-p.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BIkTNPpp.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DTcMWrd2.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqzDaNQ7.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSl_vGFe.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCA5HXM6.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
- package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DvqT18ge.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BpLQPI9C.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CzIz_-Lt.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CMQ8Jpp-.js → linear-BGKgMc2T.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BdAMrTf7.js → mermaid.core-DF6mMPJ9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-hSwF6sOq.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DZdv509B.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CmD4-b0K.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKxNxlS.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-D48iIoiS.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3N0K-2p.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CACWR-Xs.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-g_QgugvZ.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqLoRPGK.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-ES59VgMG.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-fjyLVNWo.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cd5-biKv.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CZTu6mPf.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DR01jjbe.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-iZcuGVGa.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -41
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
- package/docs/templates/backend-test-dag.json +1 -0
- package/package.json +4 -4
- package/dist/worker/console/static/assets/channel-DxgzA1GF.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +0 -1
- package/dist/worker/console/static/assets/index-BAEDjTbD.css +0 -1
- package/dist/worker/console/static/assets/index-wKEJUaP5.js +0 -482
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +0 -8
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.44.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
363
|
-
//
|
|
364
|
-
//
|
|
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
|
|
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
|
|
402
|
-
* (install verified +
|
|
403
|
-
*
|
|
404
|
-
*
|
|
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(
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
processBlocks: displayable.filter((
|
|
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
|
|
442
|
-
*
|
|
443
|
-
*
|
|
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 !==
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
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
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
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) {
|