@tea-agent/loop-agent 0.44.0-next.2 → 0.44.0-next.4
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 +5 -3
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +2 -2
- package/dist/task/config-types.js +2 -1
- package/dist/task/contract/schema.js +1 -0
- package/dist/worker/console/chat/chat-event-store.js +32 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/db-connections.js +48 -16
- package/dist/worker/console/chat/pi-runtime.js +131 -15
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/routes.js +136 -3
- package/dist/worker/console/chat/turn-process.js +128 -7
- package/dist/worker/console/prd-intake-bridge.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B8EdIjdl.js → abnfDiagram-N423BO3Z-B_lmJjOm.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CUaduV17.js → arc-pymM4Qeh.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-bYAcxRaf.js → architectureDiagram-T3A2C74G-Bw9EPAhi.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-FfhFUEMJ.js → blockDiagram-VBNYF7ZC-C7_oXJp2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-0Rqwn2_U.js → c4Diagram-5PPSVZJV-LnLWDbh8.js} +1 -1
- package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D4FbDmsM.js → chunk-2GRJ4B5K-BBiu3sCW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C2nu32ml.js → chunk-2Q5K7J3B-DiXqDQml.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-G2tHSClK.js → chunk-5RXB4S5H-lmNq9nYk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-181nyzuH.js → chunk-5VM5RSS4-boBu74dU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-D091VbiQ.js → chunk-6Q2QTUOP-CjxqoJLQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BjrMCKTH.js → chunk-GF5L2VYU-DL0vQ9MG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-T3zFHTpb.js → chunk-JWPE2WC7-DGPqE-O0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CIB7wrYq.js → chunk-KBJHAD2P-v4BbT_r2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-HwnAsgDw.js → chunk-RYQCIY6F-Ci0ynvyL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CQSdQAqo.js → chunk-XXDRQBXY-CGsbJoTV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CyBVLpjP.js → cose-bilkent-JH36ORCC-CzoRDEI7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-D2Pf3U7m.js → cynefin-VYW2F7L2-BOwFwbX4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CzXuQ1a2.js → cynefinDiagram-MW4NZA55-2Cfg451l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-9cMmcOsQ.js → dagre-VZM6K2ZE-DbT0D6Y5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DTOr8GLs.js → diagram-7IWD3JNH-Dfd_JoMs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8saqFHU.js → diagram-B4RE2ZJO-DaGm82kh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bvygp--w.js → diagram-LBJQPF4R-DMoI8Ibc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CJyLwBhZ.js → diagram-Q27KOJAE-BtIFA3aY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CkR1FDRN.js → diagram-UB23O5K3-D_2X1DTZ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BsbNtwvi.js → ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-EVZo9AKy.js → erDiagram-JOGREHBK-DBXh5Svj.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C1x91tWN.js → flowDiagram-UKHOOZJN-C4ycm8KM.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CiNBUsrJ.js → ganttDiagram-PKOTCBZU-qr-_Pekr.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CSd50p3d.js → gitGraphDiagram-DS77QQ5N-DzOSLv_A.js} +1 -1
- package/dist/worker/console/static/assets/index-PFjB6g6l.js +482 -0
- package/dist/worker/console/static/assets/index-ZtyB6Ol-.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-1JDiYCuA.js → infoDiagram-6WML65LV-C7Hl8D7l.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BZLE6zpm.js → ishikawaDiagram-WSZJBQD7-fBxVlr6t.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cg0nDmbe.js → journeyDiagram-NVQOT4AX-CoiJEVCl.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DcwEdKjq.js → kanban-definition-27J2QSJJ-TJXpxNuV.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CXvVge5x.js → linear-CjQWz1pG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-5R4WTYSN.js → mermaid.core-DY9_apIE.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-By9pSeeq.js → mindmap-definition-FAOFIHXS-HB0Id8gM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DzbVZTqW.js → pegDiagram-VL7TDLO6-0OSWgr6v.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B9nbfnAq.js → pieDiagram-7S7Q4E2Y-Cei1GGC2.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-D268jfWH.js → quadrantDiagram-CIZ2JOQS-CDy0Adfu.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-oNOXPRNn.js → railroadDiagram-AXF67PYL-BwqzUWwN.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DXdr8K-4.js → requirementDiagram-LRYGKXZP-Bif2IBRL.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Dtm4uOJ0.js → sankeyDiagram-W5VNT64P-CYIoNMLg.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CeeFiof4.js → sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CK1gARll.js → sizeCapture-X5ZJPWSS-lZic-_Bp.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-3jMoAcZw.js → stateDiagram-OKZ733FA-WCVlCqSd.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Div6kwoD.js → swimlanes-SLNWSIFB-pGccxTw_.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-C_Qp6bnh.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-B7P_6Up9.js → timeline-definition-Z64GVDOM-D29RlrQA.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-eOPaGUOX.js → vennDiagram-T6HMQDX7-01fEM1NU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-tAcDnb31.js → wardleyDiagram-T6FBY63Y-C4GeB6dq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Dg_r_785.js → xychartDiagram-ELKLHX3M-BXHrXHps.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +97 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +11 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +9 -0
- package/dist/worker/console/workspace-context.js +1 -0
- package/dist/worker/observe/static/console-theme.css +174 -0
- package/dist/worker/observe/static/console-theme.js +36 -0
- package/dist/worker/observe/static/operator-chrome.css +85 -0
- package/dist/worker/observe/static/operator-chrome.js +51 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -1
- package/dist/workflows/dag/frontend-plan-completeness.js +13 -3
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -6
- package/dist/workflows/dag/frontend-typed-event-store.js +5 -0
- package/dist/workflows/dag/frontend-writer-admission.js +1 -0
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-task.js +1 -0
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-ICokO4I1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0c0PBcA.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0c0PBcA.js +0 -1
- package/dist/worker/console/static/assets/index-DHBWEwz_.css +0 -1
- package/dist/worker/console/static/assets/index-RFWQLUE-.js +0 -478
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cr_kaxrX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bl4fQmZh.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
- Console 新增低饱和的「暮色蓝」深色与「雾光白」浅色主题,保留默认暖纸色;柔和背景、轻量选中态与独立按钮配色减少视觉刺激。顶部仅显示带「主题」悬停提示的紧凑明暗图标按钮,点击后展开「默认 / 深色 / 浅色」,选完自动收起,支持点击外部或 Escape 关闭;初始使用默认主题;即时切换对话、操作与观测界面,刷新后记住选择,浏览器存储不可用时仍可在当前页面切换。
|
|
6
|
+
- Operator Chat 消息流后续对齐:折叠摘要现在统计 retry/step/command/unknown 等 durable 过程节点,Trajectory 与 HTML 导出保留 DAG progress,并使用稳定过程身份避免直播插入导致 Thinking/工具行重挂载。
|
|
7
|
+
- Operator Chat 统一主时间线、Trajectory 与导出使用同一 DSH flow projection:Thinking、工具、重试、命令和完成态节点按事件序交错,保留 completed-turn 折叠;request/unknown 详情改为有界结构化摘要,不再回显裸节点 JSON。
|
|
8
|
+
- Operator Chat 的消息流对齐 DSH Conversation Node:模型步骤、重试、终止错误、输出上限、每次请求上下文、嵌套工具、`/sdd-bingo` 命令回执和未知事件均以稳定节点留痕;同一模型步骤的多次重试聚合为一条紧凑记录,Provider 终态错误改为轻量行且不再暴露原始响应;流式文本按动画帧合并发布,刷新和断线恢复保持同一身份与顺序,轨迹页面和导出同步展示。
|
|
9
|
+
- Operator Chat 在后台断线重连后不再丢消息:检测到中断会自动补齐缺口内容,也不会把正常的历史回放误判成丢事件而反复重连。
|
|
6
10
|
- Operator Chat 的恢复快照现在只会合并属于目标会话的乐观提交;切换会话后,旧会话尚未确认的输入不会再因延迟重连恢复而显示到新会话中。
|
|
7
11
|
- `/sdd-bingo` 成功提示现在展示上报的 Skill 与 Session ID;轨迹顶部会出现当前会话的常驻 SDD 身份栏,可一键复制、首次请求后自动更新;该上报以独立“操作”事件留痕,刷新或导出后仍在,不再伪装成工具调用。
|
|
8
12
|
- Operator Chat 发送消息后的空白光标改为 Codex 式“正在思考”,真实思考、工具过程或最终回答出现后自然接替;并修复了状态偶发显示在用户输入上方的问题。
|
|
@@ -701,5 +705,3 @@
|
|
|
701
705
|
## 历史版本
|
|
702
706
|
|
|
703
707
|
0.35.x 及更早(0.8.0 – 0.35.4-beta.0)的发布说明已归档到 [`CHANGELOG/CHANGELOG-0.35-and-earlier.md`](CHANGELOG/CHANGELOG-0.35-and-earlier.md);版本线维护边界与归档规则见 [`CHANGELOG/README.md`](CHANGELOG/README.md)。
|
|
704
|
-
|
|
705
|
-
|
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.4",
|
|
4
|
+
"gitSha": "6d1cd2f6dd13854cd48bcd4b38718a53cc24109a",
|
|
5
|
+
"builtAt": "2026-09-15T17:21:15.173Z"
|
|
6
6
|
}
|
|
@@ -6274,7 +6274,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
6274
6274
|
});
|
|
6275
6275
|
}
|
|
6276
6276
|
}
|
|
6277
|
-
const promptForMissingPhase = (missing) => session.coverageOnly
|
|
6277
|
+
const promptForMissingPhase = (missing) => `Actionable completeness findings (repair only these facts; preserve unrelated committed facts):\n${missing.map((item) => `- fact=${item.kind}${item.id ? `/${item.id}` : ""}; requirements=${item.requirementIds.join(",")}; reason=${item.reason}; owner=${item.owner ?? "Plan"}; repairScope=${item.repairScope ?? "fact"}; next=${item.nextAction ?? "replace-fact-and-finalize"}`).join("\n")}\n\n` + (session.coverageOnly
|
|
6278
6278
|
? buildCoveragePrompt(session.coverageSlice ?? [], missing)
|
|
6279
6279
|
: isCompactLocalSession
|
|
6280
6280
|
? buildCompactLocalPrompt(missing)
|
|
@@ -6282,7 +6282,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
6282
6282
|
? buildUxRegistryPrompt(missing)
|
|
6283
6283
|
: isUxLocalSession
|
|
6284
6284
|
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
6285
|
-
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
6285
|
+
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds));
|
|
6286
6286
|
const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
|
|
6287
6287
|
if (recovery === "stop")
|
|
6288
6288
|
return { ...result, ok: false };
|
|
@@ -308,9 +308,10 @@ const taskConfigObjectSchema = z.object({
|
|
|
308
308
|
.object({
|
|
309
309
|
allowed: z.array(z.string().min(1)).optional(),
|
|
310
310
|
forbidden: z.array(z.string().min(1)).optional(),
|
|
311
|
+
requiredClauses: z.array(z.string().min(1)).optional(),
|
|
311
312
|
})
|
|
312
313
|
.strict()
|
|
313
|
-
.refine((entry) => (entry.allowed?.length ?? 0) > 0 || (entry.forbidden?.length ?? 0) > 0, { message: "capabilityBoundary needs allowed or forbidden entries" })
|
|
314
|
+
.refine((entry) => (entry.allowed?.length ?? 0) > 0 || (entry.forbidden?.length ?? 0) > 0 || (entry.requiredClauses?.length ?? 0) > 0, { message: "capabilityBoundary needs allowed or forbidden entries" })
|
|
314
315
|
.optional(),
|
|
315
316
|
/** Generation-frozen interaction id vocabulary. Contract interaction names
|
|
316
317
|
* must come from this set (plan finalize deterministic check). */
|
|
@@ -327,6 +327,10 @@ export function projectChatToolEventPayload(input) {
|
|
|
327
327
|
toolName: input.toolName,
|
|
328
328
|
argsPreview: preview,
|
|
329
329
|
previewHash,
|
|
330
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
331
|
+
...(input.parentToolCallId
|
|
332
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
333
|
+
: {}),
|
|
330
334
|
};
|
|
331
335
|
}
|
|
332
336
|
const { preview, previewHash } = projectToolResultPreview(input.value);
|
|
@@ -336,9 +340,37 @@ export function projectChatToolEventPayload(input) {
|
|
|
336
340
|
resultPreview: preview,
|
|
337
341
|
previewHash,
|
|
338
342
|
isError: Boolean(input.isError),
|
|
343
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
344
|
+
...(input.parentToolCallId
|
|
345
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
346
|
+
: {}),
|
|
339
347
|
...(input.operationId ? { operationId: input.operationId } : {}),
|
|
340
348
|
};
|
|
341
349
|
}
|
|
350
|
+
export function projectUnknownChatEventPayload(input) {
|
|
351
|
+
const redact = (value, depth) => {
|
|
352
|
+
if (depth >= 6)
|
|
353
|
+
return "[DEPTH_LIMIT]";
|
|
354
|
+
if (Array.isArray(value)) {
|
|
355
|
+
return value.slice(0, 50).map((item) => redact(item, depth + 1));
|
|
356
|
+
}
|
|
357
|
+
if (!value || typeof value !== "object")
|
|
358
|
+
return value;
|
|
359
|
+
const result = {};
|
|
360
|
+
for (const [key, child] of Object.entries(value).slice(0, 100)) {
|
|
361
|
+
result[key] = /authorization|cookie|password|secret|token|api[-_]?key/i.test(key)
|
|
362
|
+
? "[REDACTED]"
|
|
363
|
+
: redact(child, depth + 1);
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
};
|
|
367
|
+
const { preview, previewHash } = boundedRedactedJson(redact(input.value, 0));
|
|
368
|
+
return {
|
|
369
|
+
eventType: input.eventType.slice(0, 120),
|
|
370
|
+
preview,
|
|
371
|
+
previewHash,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
342
374
|
export function projectOperationEventSummary(event) {
|
|
343
375
|
const messagePreview = boundedRedactedText(event.message, 600);
|
|
344
376
|
const dataPreview = event.data === undefined
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
function finiteStep(data) {
|
|
2
|
+
return typeof data.step === "number" && Number.isSafeInteger(data.step) && data.step >= 0
|
|
3
|
+
? data.step
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function locationFor(event, step = finiteStep(event.data)) {
|
|
7
|
+
return step === undefined
|
|
8
|
+
? { kind: "turn", turnId: event.turnId }
|
|
9
|
+
: { kind: "step", turnId: event.turnId, step };
|
|
10
|
+
}
|
|
11
|
+
function status(value) {
|
|
12
|
+
return value === "scheduled" ||
|
|
13
|
+
value === "started" ||
|
|
14
|
+
value === "cancelled" ||
|
|
15
|
+
value === "running" ||
|
|
16
|
+
value === "completed" ||
|
|
17
|
+
value === "failed" ||
|
|
18
|
+
value === "interrupted"
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
function positiveInteger(value) {
|
|
23
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
24
|
+
? value
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
function nonNegativeNumber(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
29
|
+
? value
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
function nonEmptyString(value) {
|
|
33
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
34
|
+
}
|
|
35
|
+
function unknownRetryNode(event, base) {
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
key: `unknown:${event.eventId}`,
|
|
39
|
+
kind: "unknown",
|
|
40
|
+
location: locationFor(event),
|
|
41
|
+
data: {
|
|
42
|
+
eventType: "model_retry",
|
|
43
|
+
preview: "Incomplete model retry lifecycle event",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function retryStateFromEvent(data) {
|
|
48
|
+
if (data.state === "scheduled")
|
|
49
|
+
return "scheduled";
|
|
50
|
+
return data.success === false || data.state === "failed"
|
|
51
|
+
? "cancelled"
|
|
52
|
+
: "started";
|
|
53
|
+
}
|
|
54
|
+
function isRetryAttempt(value) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
return false;
|
|
57
|
+
const attempt = value;
|
|
58
|
+
return (positiveInteger(attempt.attempt) !== undefined &&
|
|
59
|
+
positiveInteger(attempt.maxAttempts) !== undefined &&
|
|
60
|
+
nonNegativeNumber(attempt.delayMs) !== undefined &&
|
|
61
|
+
nonEmptyString(attempt.errorMessage) !== undefined &&
|
|
62
|
+
(attempt.state === "scheduled" ||
|
|
63
|
+
attempt.state === "started" ||
|
|
64
|
+
attempt.state === "cancelled") &&
|
|
65
|
+
typeof attempt.seq === "number" &&
|
|
66
|
+
typeof attempt.at === "number");
|
|
67
|
+
}
|
|
68
|
+
/** Read the current complete attempt from a projected retry-chain node. */
|
|
69
|
+
export function modelRetryCurrent(node) {
|
|
70
|
+
if (node.kind !== "retry")
|
|
71
|
+
return undefined;
|
|
72
|
+
return isRetryAttempt(node.data.current) ? node.data.current : undefined;
|
|
73
|
+
}
|
|
74
|
+
function modelRetryAttempts(node) {
|
|
75
|
+
return Array.isArray(node.data.attempts)
|
|
76
|
+
? node.data.attempts.filter(isRetryAttempt)
|
|
77
|
+
: [];
|
|
78
|
+
}
|
|
79
|
+
function retryUpdate(node) {
|
|
80
|
+
const value = node.data.retryUpdate;
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
82
|
+
return undefined;
|
|
83
|
+
const candidate = value;
|
|
84
|
+
if (positiveInteger(candidate.attempt) === undefined ||
|
|
85
|
+
(candidate.state !== "started" && candidate.state !== "cancelled"))
|
|
86
|
+
return undefined;
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
/** Convert one already-redacted durable event into a stable UI node. */
|
|
90
|
+
export function conversationNodeFromEvent(event) {
|
|
91
|
+
const at = Date.parse(event.at);
|
|
92
|
+
const base = {
|
|
93
|
+
anchorSeq: event.seq,
|
|
94
|
+
updatedSeq: event.seq,
|
|
95
|
+
at: Number.isFinite(at) ? at : 0,
|
|
96
|
+
data: event.data,
|
|
97
|
+
};
|
|
98
|
+
const step = finiteStep(event.data);
|
|
99
|
+
switch (event.kind) {
|
|
100
|
+
case "step_start":
|
|
101
|
+
case "step_end": {
|
|
102
|
+
if (step === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const outcome = status(event.data.outcome);
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
key: `step:${event.turnId}:${step}`,
|
|
108
|
+
kind: "step",
|
|
109
|
+
location: { kind: "step", turnId: event.turnId, step },
|
|
110
|
+
status: event.kind === "step_start" ? "running" : (outcome ?? "completed"),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
case "model_retry": {
|
|
114
|
+
const attempt = positiveInteger(event.data.attempt);
|
|
115
|
+
if (attempt === undefined)
|
|
116
|
+
return unknownRetryNode(event, base);
|
|
117
|
+
const retryState = retryStateFromEvent(event.data);
|
|
118
|
+
const key = `retry:${event.turnId}:${step ?? 0}`;
|
|
119
|
+
if (retryState !== "scheduled") {
|
|
120
|
+
return {
|
|
121
|
+
...base,
|
|
122
|
+
key,
|
|
123
|
+
kind: "retry",
|
|
124
|
+
location: locationFor(event),
|
|
125
|
+
status: retryState,
|
|
126
|
+
data: {
|
|
127
|
+
retryUpdate: {
|
|
128
|
+
attempt,
|
|
129
|
+
state: retryState,
|
|
130
|
+
seq: event.seq,
|
|
131
|
+
at: base.at,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const maxAttempts = positiveInteger(event.data.maxAttempts);
|
|
137
|
+
const delayMs = nonNegativeNumber(event.data.delayMs);
|
|
138
|
+
const errorMessage = nonEmptyString(event.data.errorMessage);
|
|
139
|
+
if (maxAttempts === undefined || delayMs === undefined || !errorMessage) {
|
|
140
|
+
return unknownRetryNode(event, base);
|
|
141
|
+
}
|
|
142
|
+
const current = {
|
|
143
|
+
attempt,
|
|
144
|
+
maxAttempts,
|
|
145
|
+
delayMs,
|
|
146
|
+
errorMessage,
|
|
147
|
+
state: "scheduled",
|
|
148
|
+
seq: event.seq,
|
|
149
|
+
at: base.at,
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
key,
|
|
154
|
+
kind: "retry",
|
|
155
|
+
location: locationFor(event),
|
|
156
|
+
status: "scheduled",
|
|
157
|
+
data: { attempts: [current], current },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
case "turn_error":
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
key: `turn-error:${event.turnId}`,
|
|
164
|
+
kind: "turn-error",
|
|
165
|
+
location: locationFor(event),
|
|
166
|
+
status: "failed",
|
|
167
|
+
};
|
|
168
|
+
case "turn_max_tokens":
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
key: `max-tokens:${event.turnId}:${step ?? 0}`,
|
|
172
|
+
kind: "max-tokens",
|
|
173
|
+
location: locationFor(event),
|
|
174
|
+
status: "completed",
|
|
175
|
+
};
|
|
176
|
+
case "request_context": {
|
|
177
|
+
const requestId = typeof event.data.requestId === "string" && event.data.requestId
|
|
178
|
+
? event.data.requestId
|
|
179
|
+
: event.eventId;
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
key: `request:${requestId}`,
|
|
183
|
+
kind: "request",
|
|
184
|
+
location: locationFor(event),
|
|
185
|
+
status: "completed",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
case "command": {
|
|
189
|
+
const commandId = typeof event.data.commandId === "string" && event.data.commandId
|
|
190
|
+
? event.data.commandId
|
|
191
|
+
: event.eventId;
|
|
192
|
+
return {
|
|
193
|
+
...base,
|
|
194
|
+
key: `command:${commandId}`,
|
|
195
|
+
kind: "command",
|
|
196
|
+
location: { kind: "turn", turnId: event.turnId },
|
|
197
|
+
status: status(event.data.outcome) ?? "completed",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "unknown":
|
|
201
|
+
return {
|
|
202
|
+
...base,
|
|
203
|
+
key: `unknown:${event.eventId}`,
|
|
204
|
+
kind: "unknown",
|
|
205
|
+
location: locationFor(event),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function conversationNodesFromEvents(events) {
|
|
210
|
+
let nodes = [];
|
|
211
|
+
for (const event of events) {
|
|
212
|
+
const node = conversationNodeFromEvent(event);
|
|
213
|
+
if (node)
|
|
214
|
+
nodes = upsertConversationNode(nodes, node);
|
|
215
|
+
}
|
|
216
|
+
return nodes;
|
|
217
|
+
}
|
|
218
|
+
/** Keyed upsert that preserves the first structural anchor and location. */
|
|
219
|
+
export function upsertConversationNode(nodes, next) {
|
|
220
|
+
if (next.kind === "retry")
|
|
221
|
+
return upsertRetryNode(nodes, next);
|
|
222
|
+
const merged = upsertBase(nodes, next);
|
|
223
|
+
if (next.kind === "step" && next.location.kind === "step") {
|
|
224
|
+
const { step: nextStep, turnId } = next.location;
|
|
225
|
+
if (next.status === "running") {
|
|
226
|
+
return transitionScheduledRetries(merged, (node) => node.location.kind === "step" &&
|
|
227
|
+
node.location.turnId === turnId &&
|
|
228
|
+
node.location.step < nextStep, "started", next.updatedSeq);
|
|
229
|
+
}
|
|
230
|
+
return transitionScheduledRetries(merged, (node) => node.location.kind === "step" &&
|
|
231
|
+
node.location.turnId === turnId &&
|
|
232
|
+
node.location.step === nextStep &&
|
|
233
|
+
node.anchorSeq < next.updatedSeq, "cancelled", next.updatedSeq);
|
|
234
|
+
}
|
|
235
|
+
if (next.kind === "turn-error") {
|
|
236
|
+
return transitionScheduledRetries(merged, (node) => node.location.kind !== "session" &&
|
|
237
|
+
next.location.kind !== "session" &&
|
|
238
|
+
node.location.turnId === next.location.turnId, "cancelled", next.updatedSeq);
|
|
239
|
+
}
|
|
240
|
+
return merged;
|
|
241
|
+
}
|
|
242
|
+
function upsertBase(nodes, next) {
|
|
243
|
+
const index = nodes.findIndex((node) => node.key === next.key);
|
|
244
|
+
if (index < 0) {
|
|
245
|
+
return [...nodes, next].sort((left, right) => left.anchorSeq - right.anchorSeq || left.key.localeCompare(right.key));
|
|
246
|
+
}
|
|
247
|
+
const previous = nodes[index];
|
|
248
|
+
if (next.updatedSeq < previous.updatedSeq)
|
|
249
|
+
return [...nodes];
|
|
250
|
+
const merged = {
|
|
251
|
+
...previous,
|
|
252
|
+
...next,
|
|
253
|
+
anchorSeq: previous.anchorSeq,
|
|
254
|
+
at: previous.at,
|
|
255
|
+
location: previous.location,
|
|
256
|
+
data: { ...previous.data, ...next.data },
|
|
257
|
+
};
|
|
258
|
+
const result = [...nodes];
|
|
259
|
+
result[index] = merged;
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function retryTurnId(node) {
|
|
263
|
+
return node.location.kind === "session" ? undefined : node.location.turnId;
|
|
264
|
+
}
|
|
265
|
+
function retryCandidateIndex(nodes, next) {
|
|
266
|
+
const exact = nodes.findIndex((node) => node.key === next.key);
|
|
267
|
+
if (exact >= 0)
|
|
268
|
+
return exact;
|
|
269
|
+
const update = retryUpdate(next);
|
|
270
|
+
const nextCurrent = modelRetryCurrent(next);
|
|
271
|
+
const attempt = update?.attempt ?? nextCurrent?.attempt;
|
|
272
|
+
const turnId = retryTurnId(next);
|
|
273
|
+
if (attempt === undefined || !turnId)
|
|
274
|
+
return -1;
|
|
275
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
276
|
+
const candidate = nodes[index];
|
|
277
|
+
const current = modelRetryCurrent(candidate);
|
|
278
|
+
if (candidate.kind !== "retry" || retryTurnId(candidate) !== turnId || !current)
|
|
279
|
+
continue;
|
|
280
|
+
if (current.attempt === attempt)
|
|
281
|
+
return index;
|
|
282
|
+
if (nextCurrent?.state === "scheduled" && current.attempt + 1 === attempt)
|
|
283
|
+
return index;
|
|
284
|
+
}
|
|
285
|
+
return -1;
|
|
286
|
+
}
|
|
287
|
+
function upsertRetryNode(nodes, next) {
|
|
288
|
+
const index = retryCandidateIndex(nodes, next);
|
|
289
|
+
const nextCurrent = modelRetryCurrent(next);
|
|
290
|
+
const update = retryUpdate(next);
|
|
291
|
+
if (index < 0) {
|
|
292
|
+
if (nextCurrent)
|
|
293
|
+
return upsertBase(nodes, next);
|
|
294
|
+
return upsertBase(nodes, {
|
|
295
|
+
...next,
|
|
296
|
+
key: `unknown:${next.key}:${next.updatedSeq}`,
|
|
297
|
+
kind: "unknown",
|
|
298
|
+
status: undefined,
|
|
299
|
+
data: {
|
|
300
|
+
eventType: "model_retry",
|
|
301
|
+
preview: "Orphan model retry lifecycle update",
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const previous = nodes[index];
|
|
306
|
+
if (next.updatedSeq < previous.updatedSeq)
|
|
307
|
+
return [...nodes];
|
|
308
|
+
let attempts = modelRetryAttempts(previous);
|
|
309
|
+
if (nextCurrent) {
|
|
310
|
+
attempts = attempts.map((attempt) => attempt.state === "scheduled" && attempt.attempt < nextCurrent.attempt
|
|
311
|
+
? { ...attempt, state: "started" }
|
|
312
|
+
: attempt);
|
|
313
|
+
const existing = attempts.findIndex((attempt) => attempt.attempt === nextCurrent.attempt);
|
|
314
|
+
if (existing >= 0)
|
|
315
|
+
attempts[existing] = { ...attempts[existing], ...nextCurrent };
|
|
316
|
+
else
|
|
317
|
+
attempts = [...attempts, nextCurrent];
|
|
318
|
+
}
|
|
319
|
+
else if (update) {
|
|
320
|
+
attempts = attempts.map((attempt) => attempt.attempt === update.attempt
|
|
321
|
+
? { ...attempt, state: update.state }
|
|
322
|
+
: attempt);
|
|
323
|
+
}
|
|
324
|
+
const current = attempts.at(-1);
|
|
325
|
+
if (!current)
|
|
326
|
+
return [...nodes];
|
|
327
|
+
const result = [...nodes];
|
|
328
|
+
result[index] = {
|
|
329
|
+
...previous,
|
|
330
|
+
updatedSeq: next.updatedSeq,
|
|
331
|
+
status: current.state,
|
|
332
|
+
data: { attempts, current },
|
|
333
|
+
};
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
function transitionScheduledRetries(nodes, predicate, state, updatedSeq) {
|
|
337
|
+
return nodes.map((node) => {
|
|
338
|
+
const current = modelRetryCurrent(node);
|
|
339
|
+
if (node.kind !== "retry" || current?.state !== "scheduled" || !predicate(node)) {
|
|
340
|
+
return node;
|
|
341
|
+
}
|
|
342
|
+
const attempts = modelRetryAttempts(node).map((attempt) => attempt.attempt === current.attempt
|
|
343
|
+
? { ...attempt, state }
|
|
344
|
+
: attempt);
|
|
345
|
+
const nextCurrent = attempts.at(-1);
|
|
346
|
+
return {
|
|
347
|
+
...node,
|
|
348
|
+
updatedSeq: Math.max(node.updatedSeq, updatedSeq),
|
|
349
|
+
status: state,
|
|
350
|
+
data: { attempts, current: nextCurrent },
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/** Resolve explicit parent links only. Cycles and excessive chains fail flat. */
|
|
355
|
+
export function toolNestingDepths(tools, maxDepth = 32) {
|
|
356
|
+
const byId = new Map(tools.map((tool) => [tool.id, tool]));
|
|
357
|
+
const depths = new Map();
|
|
358
|
+
const cyclic = new Set();
|
|
359
|
+
const resolve = (id, path) => {
|
|
360
|
+
const cached = depths.get(id);
|
|
361
|
+
if (cached !== undefined)
|
|
362
|
+
return cached;
|
|
363
|
+
const cycleAt = path.indexOf(id);
|
|
364
|
+
if (cycleAt >= 0) {
|
|
365
|
+
for (const key of path.slice(cycleAt))
|
|
366
|
+
cyclic.add(key);
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
if (path.length >= maxDepth)
|
|
370
|
+
return 0;
|
|
371
|
+
const tool = byId.get(id);
|
|
372
|
+
const parent = tool?.parentToolCallId;
|
|
373
|
+
if (!parent || !byId.has(parent) || parent === id)
|
|
374
|
+
return 0;
|
|
375
|
+
const depth = Math.min(maxDepth, resolve(parent, [...path, id]) + 1);
|
|
376
|
+
return cyclic.has(id) ? 0 : depth;
|
|
377
|
+
};
|
|
378
|
+
for (const tool of tools)
|
|
379
|
+
depths.set(tool.id, resolve(tool.id, []));
|
|
380
|
+
for (const id of cyclic)
|
|
381
|
+
depths.set(id, 0);
|
|
382
|
+
return depths;
|
|
383
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Operator Chat — governed database connection registry (2026-09-12).
|
|
3
3
|
*
|
|
4
4
|
* Manages NAMED database connection configs persisted in Console app-data
|
|
5
|
-
* (`db/connections.json` under the
|
|
5
|
+
* (`db/connections.json` under the Console-global app-data namespace). Credentials NEVER live in
|
|
6
6
|
* the config: sqlite stores an absolute file path; postgres/mysql store safe
|
|
7
7
|
* host/port/database/user metadata or the NAME of a DSN environment variable.
|
|
8
8
|
* Password/DSN values are never persisted, echoed, listed, or exported.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import os from "node:os";
|
|
17
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
19
19
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from "node:crypto";
|
|
20
20
|
import { writeSecureJson } from "../../../infrastructure/console/app-data.js";
|
|
@@ -243,14 +243,18 @@ export class DbConnectionRegistry {
|
|
|
243
243
|
handles = new Map();
|
|
244
244
|
runtimePasswords = new Map();
|
|
245
245
|
credentialVault;
|
|
246
|
+
legacyCredentialVault;
|
|
247
|
+
legacyRoot;
|
|
246
248
|
loaded = false;
|
|
247
249
|
write;
|
|
248
250
|
mutationQueue = Promise.resolve();
|
|
249
|
-
constructor(root, persistWriter = writeSecureJson, driverLoader = (moduleId) => import(moduleId)) {
|
|
251
|
+
constructor(root, persistWriter = writeSecureJson, driverLoader = (moduleId) => import(moduleId), legacyRoot) {
|
|
250
252
|
this.root = root;
|
|
251
253
|
this.persistWriter = persistWriter;
|
|
252
254
|
this.driverLoader = driverLoader;
|
|
255
|
+
this.legacyRoot = legacyRoot && path.resolve(legacyRoot) !== path.resolve(root) ? legacyRoot : undefined;
|
|
253
256
|
this.credentialVault = new LocalEncryptedCredentialVault(root);
|
|
257
|
+
this.legacyCredentialVault = this.legacyRoot ? new LocalEncryptedCredentialVault(this.legacyRoot) : undefined;
|
|
254
258
|
}
|
|
255
259
|
/** Wait for the latest persistence write to settle (tests / graceful stop). */
|
|
256
260
|
async flushed() {
|
|
@@ -264,10 +268,25 @@ export class DbConnectionRegistry {
|
|
|
264
268
|
* remove): the persisted file is small JSON, so a sync read on first use
|
|
265
269
|
* keeps list()/get() synchronous. connect() keeps the async reader.
|
|
266
270
|
*/
|
|
267
|
-
ensureLoadedSync() {
|
|
268
|
-
if (this.loaded)
|
|
271
|
+
ensureLoadedSync(force = false) {
|
|
272
|
+
if (this.loaded && !force)
|
|
269
273
|
return;
|
|
270
274
|
const file = this.connectionsFile();
|
|
275
|
+
if (!existsSync(file) && this.legacyRoot) {
|
|
276
|
+
const legacyFile = path.join(this.legacyRoot, "connections.json");
|
|
277
|
+
if (existsSync(legacyFile)) {
|
|
278
|
+
try {
|
|
279
|
+
this.configs = normalizePersisted(JSON.parse(readFileSync(legacyFile, "utf8")));
|
|
280
|
+
mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
281
|
+
writeFileSync(file, JSON.stringify([...this.configs.values()], null, 2) + "\n", { mode: 0o600 });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
this.configs = new Map();
|
|
285
|
+
}
|
|
286
|
+
this.loaded = true;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
271
290
|
if (!existsSync(file)) {
|
|
272
291
|
this.configs = new Map();
|
|
273
292
|
this.loaded = true;
|
|
@@ -275,11 +294,19 @@ export class DbConnectionRegistry {
|
|
|
275
294
|
}
|
|
276
295
|
try {
|
|
277
296
|
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
278
|
-
|
|
297
|
+
const next = normalizePersisted(raw);
|
|
298
|
+
for (const [name, current] of this.configs) {
|
|
299
|
+
const updated = next.get(name);
|
|
300
|
+
if (!updated || JSON.stringify(updated) !== JSON.stringify(current))
|
|
301
|
+
this.closeHandle(name);
|
|
302
|
+
}
|
|
303
|
+
this.configs = next;
|
|
279
304
|
}
|
|
280
305
|
catch {
|
|
281
|
-
//
|
|
282
|
-
|
|
306
|
+
// A concurrent atomic replace can make the file briefly unavailable.
|
|
307
|
+
// Keep the last valid snapshot instead of hiding every connection.
|
|
308
|
+
if (!this.loaded)
|
|
309
|
+
this.configs = new Map();
|
|
283
310
|
}
|
|
284
311
|
this.loaded = true;
|
|
285
312
|
}
|
|
@@ -307,7 +334,7 @@ export class DbConnectionRegistry {
|
|
|
307
334
|
}
|
|
308
335
|
async add(input) {
|
|
309
336
|
return this.mutate(async () => {
|
|
310
|
-
this.ensureLoadedSync();
|
|
337
|
+
this.ensureLoadedSync(true);
|
|
311
338
|
const config = normalizeConnectionInput(input);
|
|
312
339
|
if (config.mode === "parameters" && (config.passwordMode === "runtime" || config.passwordMode === "encrypted") && typeof input.password !== "string") {
|
|
313
340
|
throw new DbConnectionError("DB_INVALID_CONFIG", "password is required for a new password connection");
|
|
@@ -330,7 +357,7 @@ export class DbConnectionRegistry {
|
|
|
330
357
|
}
|
|
331
358
|
async update(currentName, input) {
|
|
332
359
|
return this.mutate(async () => {
|
|
333
|
-
this.ensureLoadedSync();
|
|
360
|
+
this.ensureLoadedSync(true);
|
|
334
361
|
const current = this.configs.get(currentName);
|
|
335
362
|
if (!current) {
|
|
336
363
|
throw new DbConnectionError("DB_CONNECTION_NOT_FOUND", `no database connection named "${currentName}"`);
|
|
@@ -372,7 +399,7 @@ export class DbConnectionRegistry {
|
|
|
372
399
|
}
|
|
373
400
|
async remove(name) {
|
|
374
401
|
return this.mutate(async () => {
|
|
375
|
-
this.ensureLoadedSync();
|
|
402
|
+
this.ensureLoadedSync(true);
|
|
376
403
|
const current = this.configs.get(name);
|
|
377
404
|
if (!current)
|
|
378
405
|
return false;
|
|
@@ -388,7 +415,7 @@ export class DbConnectionRegistry {
|
|
|
388
415
|
});
|
|
389
416
|
}
|
|
390
417
|
exportConfig() {
|
|
391
|
-
this.ensureLoadedSync();
|
|
418
|
+
this.ensureLoadedSync(true);
|
|
392
419
|
return {
|
|
393
420
|
schemaVersion: 1,
|
|
394
421
|
exportedAt: new Date().toISOString(),
|
|
@@ -397,7 +424,7 @@ export class DbConnectionRegistry {
|
|
|
397
424
|
}
|
|
398
425
|
async importConfig(input, conflict = "skip") {
|
|
399
426
|
return this.mutate(async () => {
|
|
400
|
-
this.ensureLoadedSync();
|
|
427
|
+
this.ensureLoadedSync(true);
|
|
401
428
|
if (!isRecord(input) || input.schemaVersion !== 1 || !Array.isArray(input.connections)) {
|
|
402
429
|
throw new DbConnectionError("DB_INVALID_CONFIG", "invalid database connection export document");
|
|
403
430
|
}
|
|
@@ -470,11 +497,11 @@ export class DbConnectionRegistry {
|
|
|
470
497
|
}
|
|
471
498
|
/** Sanitized summaries — never includes any env var or password value. */
|
|
472
499
|
list() {
|
|
473
|
-
this.ensureLoadedSync();
|
|
500
|
+
this.ensureLoadedSync(true);
|
|
474
501
|
return [...this.configs.values()].map((config) => this.summary(config));
|
|
475
502
|
}
|
|
476
503
|
get(name) {
|
|
477
|
-
this.ensureLoadedSync();
|
|
504
|
+
this.ensureLoadedSync(true);
|
|
478
505
|
const config = this.configs.get(name);
|
|
479
506
|
return config ? this.summary(config) : undefined;
|
|
480
507
|
}
|
|
@@ -542,7 +569,7 @@ export class DbConnectionRegistry {
|
|
|
542
569
|
};
|
|
543
570
|
}
|
|
544
571
|
getConfig(name) {
|
|
545
|
-
this.ensureLoadedSync();
|
|
572
|
+
this.ensureLoadedSync(true);
|
|
546
573
|
const config = this.configs.get(name);
|
|
547
574
|
if (!config)
|
|
548
575
|
throw new DbConnectionError("DB_CONNECTION_NOT_FOUND", `no database connection named "${name}"`);
|
|
@@ -606,6 +633,11 @@ export class DbConnectionRegistry {
|
|
|
606
633
|
}
|
|
607
634
|
else if (config.passwordMode === "encrypted") {
|
|
608
635
|
password = config.passwordRef ? await this.credentialVault.get(config.passwordRef) : undefined;
|
|
636
|
+
if (password === undefined && this.legacyCredentialVault && config.passwordRef) {
|
|
637
|
+
password = await this.legacyCredentialVault.get(config.passwordRef);
|
|
638
|
+
if (password !== undefined)
|
|
639
|
+
await this.credentialVault.set(config.passwordRef, password);
|
|
640
|
+
}
|
|
609
641
|
if (password === undefined) {
|
|
610
642
|
throw new DbConnectionError("DB_ENV_MISSING", `encrypted password for connection "${config.name}" is not configured; edit the connection and enter it again`);
|
|
611
643
|
}
|