@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.
- package/CHANGELOG.md +285 -454
- package/dist/build-stamp.json +3 -3
- package/dist/shared/git-workspace-status.js +2 -0
- 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 +34 -0
- package/dist/worker/console/chat/routes.js +138 -9
- 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/static/assets/{abnfDiagram-N423BO3Z-Brn2sPzL.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BIMNTG0W.js → arc-CQbCsSqb.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G--WtcgR_A.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BT_Ztog1.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5hbVr-l.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-BKKnyR9Z.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BHs6biqA.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BdSFy_vP.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpJIdTKV.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C_iWJhkT.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BTbmx3-k.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D4Ee6DmP.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B487SORR.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BKYHjNrr.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DrTdoYZG.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-CttUY_Jo.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B4Rm5gnR.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-ByxHBq_S.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DGrfNFA0.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-KjrUPquv.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-VS4mlm75.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BLXYmFTM.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C6xoFAqH.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C478dCe7.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DHEpW5zN.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CA5GKfo9.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-zysi0Fl-.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGzcKA8Y.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CHsuIG1w.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-BexZHPCp.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-XDW3qrJ7.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ07eEaN.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DL-zeqOn.js → linear-BGKgMc2T.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C0HdXtOf.js → mermaid.core-DF6mMPJ9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CqRX0lrt.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVM9JZ2H.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BA3ozdKk.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B-0_p9_4.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DHRzPWFc.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-9BEC_PcW.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DMxeW4rN.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DixP6LIU.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B_EifJBo.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BwikwWbX.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-C162l9o1.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-mpe_u1i4.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DyUk54HX.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-HYyMkzfm.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DRoh7skR.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- 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/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 -43
- 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/useRepoBrowser.js +5 -2
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DNWB4hpR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +0 -1
- package/dist/worker/console/static/assets/index-CJh6pVLm.js +0 -482
- package/dist/worker/console/static/assets/index-C_kvKmFM.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.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
|
}
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -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",
|