@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.2
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 +135 -92
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +12 -1
- package/dist/task/source-prepare/fragment-inventory.js +1 -14
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +94 -64
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
- package/dist/workflows/dag/init-hybrid.js +259 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/product-line/task.yaml +1 -1
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -53,6 +53,9 @@ export function hydrateSessionThread(raw) {
|
|
|
53
53
|
role: "assistant",
|
|
54
54
|
text: item.text,
|
|
55
55
|
createdAt,
|
|
56
|
+
...(item.kind === "provider-error" && item.providerError
|
|
57
|
+
? { kind: item.kind, providerError: item.providerError }
|
|
58
|
+
: {}),
|
|
56
59
|
...(content && content.length > 0 ? { content } : {}),
|
|
57
60
|
});
|
|
58
61
|
flushPendingToAssistant(item.id);
|
|
@@ -154,9 +157,23 @@ function appendAssistantUnmatchedTools(group, assistant, processItems, usedToolI
|
|
|
154
157
|
function fillProcessDetails(group) {
|
|
155
158
|
const usedToolIds = new Set();
|
|
156
159
|
const processItems = [];
|
|
157
|
-
const
|
|
158
|
-
const finalAssistant =
|
|
159
|
-
|
|
160
|
+
const lastAssistant = group.assistants.at(-1);
|
|
161
|
+
const finalAssistant = lastAssistant?.kind === "provider-error" ? undefined : lastAssistant;
|
|
162
|
+
const finalIndex = finalAssistant
|
|
163
|
+
? group.assistants.lastIndexOf(finalAssistant)
|
|
164
|
+
: -1;
|
|
165
|
+
for (const [index, assistant] of group.assistants.entries()) {
|
|
166
|
+
if (index === finalIndex)
|
|
167
|
+
continue;
|
|
168
|
+
if (assistant.kind === "provider-error" && assistant.providerError) {
|
|
169
|
+
emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
|
|
170
|
+
appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
|
|
171
|
+
processItems.push({
|
|
172
|
+
kind: "provider-error",
|
|
173
|
+
error: assistant.providerError,
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
160
177
|
emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
|
|
161
178
|
appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
|
|
162
179
|
}
|
|
@@ -292,55 +309,3 @@ export function selectRecentTurnGroups(groups, limit) {
|
|
|
292
309
|
return [...groups];
|
|
293
310
|
return groups.slice(-limit);
|
|
294
311
|
}
|
|
295
|
-
export function processGroupSummary(tools, options) {
|
|
296
|
-
const count = tools.length;
|
|
297
|
-
const thinkingCount = options?.thinkingCount ?? 0;
|
|
298
|
-
const textCount = options?.textCount ?? 0;
|
|
299
|
-
const activityCount = options?.activityCount ?? 0;
|
|
300
|
-
const stepCount = thinkingCount + textCount + activityCount + count;
|
|
301
|
-
const errorCount = tools.filter((t) => t.status === "error").length +
|
|
302
|
-
(options?.activityErrorCount ?? 0);
|
|
303
|
-
const runningCount = tools.filter((t) => t.status === "running").length +
|
|
304
|
-
(options?.activityRunningCount ?? 0);
|
|
305
|
-
const parts = [`处理详情 · ${stepCount} 步`];
|
|
306
|
-
if (count > 0)
|
|
307
|
-
parts.push(`${count} 工具`);
|
|
308
|
-
if (activityCount > 0)
|
|
309
|
-
parts.push(`${activityCount} 进展`);
|
|
310
|
-
if (errorCount > 0)
|
|
311
|
-
parts.push(`${errorCount} 出错`);
|
|
312
|
-
else if (runningCount > 0)
|
|
313
|
-
parts.push("运行中");
|
|
314
|
-
return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
|
|
315
|
-
}
|
|
316
|
-
/** Summary over rendered process items. */
|
|
317
|
-
export function summarizeProcessItems(items) {
|
|
318
|
-
const tools = [];
|
|
319
|
-
let thinkingCount = 0;
|
|
320
|
-
let textCount = 0;
|
|
321
|
-
let activityCount = 0;
|
|
322
|
-
let activityErrorCount = 0;
|
|
323
|
-
let activityRunningCount = 0;
|
|
324
|
-
for (const item of items) {
|
|
325
|
-
if (item.kind === "thinking")
|
|
326
|
-
thinkingCount += 1;
|
|
327
|
-
else if (item.kind === "text")
|
|
328
|
-
textCount += 1;
|
|
329
|
-
else if (item.kind === "activity") {
|
|
330
|
-
activityCount += 1;
|
|
331
|
-
if (item.activity.kind === "error")
|
|
332
|
-
activityErrorCount += 1;
|
|
333
|
-
else if (item.activity.kind === "progress")
|
|
334
|
-
activityRunningCount += 1;
|
|
335
|
-
}
|
|
336
|
-
else
|
|
337
|
-
tools.push(item.tool);
|
|
338
|
-
}
|
|
339
|
-
return processGroupSummary(tools, {
|
|
340
|
-
thinkingCount,
|
|
341
|
-
textCount,
|
|
342
|
-
activityCount,
|
|
343
|
-
activityErrorCount,
|
|
344
|
-
activityRunningCount,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJsonIfExists, writeSecureJson, } from "../../../infrastructure/console/app-data.js";
|
|
4
|
+
function questionFile(root, sessionId) {
|
|
5
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 180);
|
|
6
|
+
return path.join(root, `${safe}.json`);
|
|
7
|
+
}
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function normalizeQuestions(value) {
|
|
12
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
|
|
13
|
+
throw new Error("ask_user_question requires 1-20 questions");
|
|
14
|
+
}
|
|
15
|
+
const ids = new Set();
|
|
16
|
+
return value.map((raw, index) => {
|
|
17
|
+
if (!isRecord(raw))
|
|
18
|
+
throw new Error(`question ${index + 1} must be an object`);
|
|
19
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
20
|
+
const question = typeof raw.question === "string" ? raw.question.trim() : "";
|
|
21
|
+
if (!id || !question)
|
|
22
|
+
throw new Error(`question ${index + 1} requires id and question`);
|
|
23
|
+
if (ids.has(id))
|
|
24
|
+
throw new Error(`duplicate question id: ${id}`);
|
|
25
|
+
ids.add(id);
|
|
26
|
+
const options = raw.options === undefined ? undefined : normalizeOptions(raw.options, id);
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
question,
|
|
30
|
+
...(typeof raw.header === "string" && raw.header.trim()
|
|
31
|
+
? { header: raw.header.trim().slice(0, 80) }
|
|
32
|
+
: {}),
|
|
33
|
+
...(typeof raw.detail === "string" && raw.detail.trim()
|
|
34
|
+
? { detail: raw.detail.trim().slice(0, 4000) }
|
|
35
|
+
: {}),
|
|
36
|
+
...(options ? { options } : {}),
|
|
37
|
+
...(raw.multi_select === true ? { multiSelect: true } : {}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function normalizeOptions(value, questionId) {
|
|
42
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 12) {
|
|
43
|
+
throw new Error(`question ${questionId} options must contain 1-12 items`);
|
|
44
|
+
}
|
|
45
|
+
const labels = new Set();
|
|
46
|
+
return value.map((raw, index) => {
|
|
47
|
+
if (!isRecord(raw) || typeof raw.label !== "string" || !raw.label.trim()) {
|
|
48
|
+
throw new Error(`question ${questionId} option ${index + 1} requires a label`);
|
|
49
|
+
}
|
|
50
|
+
const label = raw.label.trim();
|
|
51
|
+
if (labels.has(label))
|
|
52
|
+
throw new Error(`duplicate option label: ${label}`);
|
|
53
|
+
labels.add(label);
|
|
54
|
+
return {
|
|
55
|
+
label,
|
|
56
|
+
...(typeof raw.description === "string" && raw.description.trim()
|
|
57
|
+
? { description: raw.description.trim().slice(0, 300) }
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function normalizeAnswer(value, questions) {
|
|
63
|
+
if (!isRecord(value) || !Array.isArray(value.answers))
|
|
64
|
+
throw new Error("answers must be an array");
|
|
65
|
+
const byId = new Map(questions.map((question) => [question.id, question]));
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const answers = value.answers.map((raw) => {
|
|
68
|
+
if (!isRecord(raw) || typeof raw.id !== "string")
|
|
69
|
+
throw new Error("answer id is required");
|
|
70
|
+
const id = raw.id.trim();
|
|
71
|
+
const question = byId.get(id);
|
|
72
|
+
if (!question || seen.has(id))
|
|
73
|
+
throw new Error(`answer does not match question: ${id}`);
|
|
74
|
+
seen.add(id);
|
|
75
|
+
const selected = Array.isArray(raw.selected)
|
|
76
|
+
? raw.selected.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
|
|
77
|
+
: [];
|
|
78
|
+
const allowed = new Set((question.options ?? []).map((option) => option.label));
|
|
79
|
+
if (selected.some((item) => !allowed.has(item)))
|
|
80
|
+
throw new Error(`answer contains an unknown option for ${id}`);
|
|
81
|
+
if (!question.multiSelect && selected.length > 1)
|
|
82
|
+
throw new Error(`question ${id} accepts one option`);
|
|
83
|
+
const custom = typeof raw.custom === "string" ? raw.custom.trim().slice(0, 4000) : "";
|
|
84
|
+
return { id, selected, ...(custom ? { custom } : {}) };
|
|
85
|
+
});
|
|
86
|
+
if (seen.size !== questions.length)
|
|
87
|
+
throw new Error("every question requires an answer or skip entry");
|
|
88
|
+
return { answers };
|
|
89
|
+
}
|
|
90
|
+
export class UserQuestionRegistry {
|
|
91
|
+
root;
|
|
92
|
+
pending = new Map();
|
|
93
|
+
/** Serialize first-write and resolve/cancel writes for one session. */
|
|
94
|
+
persistence = new Map();
|
|
95
|
+
waiters = new Map();
|
|
96
|
+
turnIds = new Map();
|
|
97
|
+
listeners = new Set();
|
|
98
|
+
constructor(root) {
|
|
99
|
+
this.root = root;
|
|
100
|
+
}
|
|
101
|
+
setTurn(sessionId, turnId) {
|
|
102
|
+
this.turnIds.set(sessionId, turnId);
|
|
103
|
+
}
|
|
104
|
+
clearTurn(sessionId) {
|
|
105
|
+
this.turnIds.delete(sessionId);
|
|
106
|
+
}
|
|
107
|
+
subscribe(listener) {
|
|
108
|
+
this.listeners.add(listener);
|
|
109
|
+
return () => this.listeners.delete(listener);
|
|
110
|
+
}
|
|
111
|
+
async get(sessionId) {
|
|
112
|
+
const memory = this.pending.get(sessionId);
|
|
113
|
+
if (memory?.status === "pending")
|
|
114
|
+
return structuredClone(memory);
|
|
115
|
+
const disk = await readJsonIfExists(questionFile(this.root, sessionId));
|
|
116
|
+
if (disk?.status === "pending") {
|
|
117
|
+
this.pending.set(sessionId, disk);
|
|
118
|
+
return structuredClone(disk);
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
async ask(input) {
|
|
123
|
+
const questions = normalizeQuestions(input.questions);
|
|
124
|
+
if (input.signal?.aborted)
|
|
125
|
+
throw new Error("USER_QUESTION_ABORTED");
|
|
126
|
+
if (await this.get(input.sessionId))
|
|
127
|
+
throw new Error("USER_QUESTION_ALREADY_PENDING");
|
|
128
|
+
// Two tool calls can finish the disk lookup in the same turn. Re-check the
|
|
129
|
+
// in-memory lease after that await so only the first one can publish.
|
|
130
|
+
if (this.pending.get(input.sessionId)?.status === "pending") {
|
|
131
|
+
throw new Error("USER_QUESTION_ALREADY_PENDING");
|
|
132
|
+
}
|
|
133
|
+
const now = new Date().toISOString();
|
|
134
|
+
const question = {
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
requestId: `question_${randomBytes(12).toString("hex")}`,
|
|
137
|
+
sessionId: input.sessionId,
|
|
138
|
+
toolCallId: input.toolCallId,
|
|
139
|
+
...(this.turnIds.get(input.sessionId) ? { turnId: this.turnIds.get(input.sessionId) } : {}),
|
|
140
|
+
questions,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
status: "pending",
|
|
144
|
+
version: 1,
|
|
145
|
+
};
|
|
146
|
+
this.pending.set(input.sessionId, question);
|
|
147
|
+
// Register the waiter before the first persistence await. A turn abort can
|
|
148
|
+
// arrive while the request is being written; registering afterwards leaves
|
|
149
|
+
// cancel() with nobody to reject and the tool promise would hang forever.
|
|
150
|
+
let persisted = false;
|
|
151
|
+
let abortRequested = false;
|
|
152
|
+
const waiting = new Promise((resolve, reject) => {
|
|
153
|
+
const onAbort = () => {
|
|
154
|
+
abortRequested = true;
|
|
155
|
+
if (persisted) {
|
|
156
|
+
void this.cancel(input.sessionId, question.requestId, "TURN_ABORTED").catch(reject);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
this.waiters.set(input.sessionId, {
|
|
160
|
+
resolve: (answer) => {
|
|
161
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
162
|
+
resolve(answer);
|
|
163
|
+
},
|
|
164
|
+
reject: (error) => {
|
|
165
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
166
|
+
reject(error);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
if (input.signal?.aborted)
|
|
170
|
+
onAbort();
|
|
171
|
+
else
|
|
172
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
await this.persist(input.sessionId, question);
|
|
176
|
+
persisted = true;
|
|
177
|
+
if (abortRequested) {
|
|
178
|
+
await this.cancel(input.sessionId, question.requestId, "TURN_ABORTED");
|
|
179
|
+
return waiting;
|
|
180
|
+
}
|
|
181
|
+
this.emit({ type: "requested", question });
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const waiter = this.waiters.get(input.sessionId);
|
|
185
|
+
this.pending.delete(input.sessionId);
|
|
186
|
+
this.waiters.delete(input.sessionId);
|
|
187
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
188
|
+
if (waiter) {
|
|
189
|
+
waiter.reject(failure);
|
|
190
|
+
return waiting;
|
|
191
|
+
}
|
|
192
|
+
throw failure;
|
|
193
|
+
}
|
|
194
|
+
return waiting;
|
|
195
|
+
}
|
|
196
|
+
async answer(sessionId, requestId, rawAnswer) {
|
|
197
|
+
await this.persistence.get(sessionId);
|
|
198
|
+
const current = await this.get(sessionId);
|
|
199
|
+
if (!current)
|
|
200
|
+
throw new Error("USER_QUESTION_NOT_FOUND");
|
|
201
|
+
if (current.requestId !== requestId)
|
|
202
|
+
throw new Error("USER_QUESTION_STALE");
|
|
203
|
+
const answer = normalizeAnswer(rawAnswer, current.questions);
|
|
204
|
+
const resolved = {
|
|
205
|
+
...current,
|
|
206
|
+
status: "resolved",
|
|
207
|
+
answer,
|
|
208
|
+
updatedAt: new Date().toISOString(),
|
|
209
|
+
version: current.version + 1,
|
|
210
|
+
};
|
|
211
|
+
this.pending.delete(sessionId);
|
|
212
|
+
await this.persist(sessionId, resolved);
|
|
213
|
+
this.emit({ type: "resolved", question: resolved });
|
|
214
|
+
const waiter = this.waiters.get(sessionId);
|
|
215
|
+
this.waiters.delete(sessionId);
|
|
216
|
+
waiter?.resolve(answer);
|
|
217
|
+
return resolved;
|
|
218
|
+
}
|
|
219
|
+
async cancel(sessionId, requestId, reason = "USER_CANCELLED") {
|
|
220
|
+
await this.persistence.get(sessionId);
|
|
221
|
+
const current = await this.get(sessionId);
|
|
222
|
+
if (!current)
|
|
223
|
+
throw new Error("USER_QUESTION_NOT_FOUND");
|
|
224
|
+
if (current.requestId !== requestId)
|
|
225
|
+
throw new Error("USER_QUESTION_STALE");
|
|
226
|
+
const cancelled = {
|
|
227
|
+
...current,
|
|
228
|
+
status: "cancelled",
|
|
229
|
+
cancelReason: reason,
|
|
230
|
+
updatedAt: new Date().toISOString(),
|
|
231
|
+
version: current.version + 1,
|
|
232
|
+
};
|
|
233
|
+
this.pending.delete(sessionId);
|
|
234
|
+
await this.persist(sessionId, cancelled);
|
|
235
|
+
this.emit({ type: "cancelled", question: cancelled });
|
|
236
|
+
const waiter = this.waiters.get(sessionId);
|
|
237
|
+
this.waiters.delete(sessionId);
|
|
238
|
+
waiter?.reject(new Error(reason));
|
|
239
|
+
return cancelled;
|
|
240
|
+
}
|
|
241
|
+
async cancelForSession(sessionId, reason = "SESSION_DISPOSED") {
|
|
242
|
+
const current = await this.get(sessionId);
|
|
243
|
+
if (current)
|
|
244
|
+
await this.cancel(sessionId, current.requestId, reason);
|
|
245
|
+
}
|
|
246
|
+
emit(change) {
|
|
247
|
+
for (const listener of this.listeners) {
|
|
248
|
+
try {
|
|
249
|
+
listener(change);
|
|
250
|
+
}
|
|
251
|
+
catch { /* observers never break the waiter */ }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async persist(sessionId, question) {
|
|
255
|
+
const write = writeSecureJson(questionFile(this.root, sessionId), question);
|
|
256
|
+
this.persistence.set(sessionId, write);
|
|
257
|
+
try {
|
|
258
|
+
await write;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
if (this.persistence.get(sessionId) === write) {
|
|
262
|
+
this.persistence.delete(sessionId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
export const ACTIVE_CHAT_SESSION_STORAGE_KEY = "loop-console.operator-chat.active-session.v1";
|
|
2
|
+
export const ACTIVE_CHAT_SESSION_BY_WORKSPACE_STORAGE_KEY = "loop-console.operator-chat.active-session-by-workspace.v1";
|
|
2
3
|
export const DEFAULT_LANDING_STORAGE_KEY = "loop-console.operator-chat.default-landing.v1";
|
|
4
|
+
function readWorkspaceSessionMap(storage) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(storage.getItem(ACTIVE_CHAT_SESSION_BY_WORKSPACE_STORAGE_KEY) ?? "{}");
|
|
7
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
8
|
+
return {};
|
|
9
|
+
return Object.fromEntries(Object.entries(parsed).filter((entry) => Boolean(entry[0].trim()) &&
|
|
10
|
+
typeof entry[1] === "string" &&
|
|
11
|
+
Boolean(entry[1].trim())));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Resolve the selected Chat session from client state. The workspace map is
|
|
18
|
+
* authoritative; the former global key is only a safe, server-validated
|
|
19
|
+
* migration fallback for users upgrading from the single-selection layout. */
|
|
20
|
+
export function readActiveChatSessionId(storage, workspaceId) {
|
|
21
|
+
const scope = workspaceId?.trim();
|
|
22
|
+
if (scope) {
|
|
23
|
+
const map = readWorkspaceSessionMap(storage);
|
|
24
|
+
const scoped = map[scope]?.trim();
|
|
25
|
+
if (scoped)
|
|
26
|
+
return scoped;
|
|
27
|
+
// Once the map exists, an absent workspace means "no selected session";
|
|
28
|
+
// never fall back to another workspace's global legacy selection.
|
|
29
|
+
if (Object.keys(map).length > 0)
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return storage.getItem(ACTIVE_CHAT_SESSION_STORAGE_KEY)?.trim() || null;
|
|
33
|
+
}
|
|
34
|
+
/** Remember the active session for its owning workspace while retaining the
|
|
35
|
+
* global key for existing non-Chat consumers that refer to the visible session. */
|
|
36
|
+
export function rememberActiveChatSession(storage, workspaceId, sessionId) {
|
|
37
|
+
const id = sessionId.trim();
|
|
38
|
+
if (!id)
|
|
39
|
+
return;
|
|
40
|
+
storage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, id);
|
|
41
|
+
const scope = workspaceId?.trim();
|
|
42
|
+
if (!scope)
|
|
43
|
+
return;
|
|
44
|
+
const map = readWorkspaceSessionMap(storage);
|
|
45
|
+
map[scope] = id;
|
|
46
|
+
storage.setItem(ACTIVE_CHAT_SESSION_BY_WORKSPACE_STORAGE_KEY, JSON.stringify(map));
|
|
47
|
+
}
|
|
48
|
+
/** Forget one workspace's invalid/deleted selection without disturbing the
|
|
49
|
+
* last session remembered for any other workspace. */
|
|
50
|
+
export function forgetActiveChatSession(storage, workspaceId, sessionId) {
|
|
51
|
+
const scope = workspaceId?.trim();
|
|
52
|
+
if (scope) {
|
|
53
|
+
const map = readWorkspaceSessionMap(storage);
|
|
54
|
+
if (!sessionId || map[scope] === sessionId)
|
|
55
|
+
delete map[scope];
|
|
56
|
+
if (Object.keys(map).length > 0) {
|
|
57
|
+
storage.setItem(ACTIVE_CHAT_SESSION_BY_WORKSPACE_STORAGE_KEY, JSON.stringify(map));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
storage.removeItem(ACTIVE_CHAT_SESSION_BY_WORKSPACE_STORAGE_KEY);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const global = storage.getItem(ACTIVE_CHAT_SESSION_STORAGE_KEY)?.trim();
|
|
64
|
+
if (!sessionId || global === sessionId) {
|
|
65
|
+
storage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
3
68
|
function configuredLanding(value) {
|
|
4
69
|
const normalized = value?.trim().toLowerCase();
|
|
5
70
|
if (normalized === "chat")
|
|
@@ -28,6 +93,20 @@ function readWorkspaceParams(query) {
|
|
|
28
93
|
...(read("fromNodeId") ? { fromNodeId: read("fromNodeId") } : {}),
|
|
29
94
|
...(read("featureId") ? { featureId: read("featureId") } : {}),
|
|
30
95
|
...(read("intent") ? { intent: read("intent") } : {}),
|
|
96
|
+
...(read("workspaceId") ? { workspaceId: read("workspaceId") } : {}),
|
|
97
|
+
...(read("sessionId") ? { sessionId: read("sessionId") } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function resolveLegacyWorkspaceSelection(location, knownWorkspaceIds) {
|
|
101
|
+
const parsed = parseConsoleRoute(location);
|
|
102
|
+
const requested = parsed?.workspaceId?.trim();
|
|
103
|
+
if (parsed && requested && !knownWorkspaceIds.includes(requested)) {
|
|
104
|
+
return { status: "unknown-workspace", workspaceId: requested };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
status: "ok",
|
|
108
|
+
workspace: parsed?.workspace ?? resolveDefaultLanding({ location }),
|
|
109
|
+
params: parsed ?? {},
|
|
31
110
|
};
|
|
32
111
|
}
|
|
33
112
|
/** Parse `#/tasks?taskId=…` hash routes (Observe-style virtual paths). */
|
|
@@ -4,7 +4,7 @@ import { createNpmUpdateClient } from "../../shared/update/npm-client.js";
|
|
|
4
4
|
import { acquireUpdateLock, updateLockPath } from "../../shared/update/state.js";
|
|
5
5
|
import { readInitRuntimeActivity } from "../observability/init-runtime-activity.js";
|
|
6
6
|
import { performConsoleUpdateAndInit, } from "./console-update-and-init.js";
|
|
7
|
-
import { TERMINAL_OPERATION_STATES } from "
|
|
7
|
+
import { TERMINAL_OPERATION_STATES } from "../../infrastructure/console/operation-store.js";
|
|
8
8
|
export function createConsoleUpdateExecutor(input) {
|
|
9
9
|
return async (channel) => {
|
|
10
10
|
const currentVersion = readPackageVersion(input.packageRoot);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { appendFile, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { assertSafeAppDataPath, readJsonIfExists, writeSecureJson, } from "../../infrastructure/console/app-data.js";
|
|
5
|
+
const diagnosticWriteChains = new Map();
|
|
6
|
+
function serializeDiagnostic(filePath, operation) {
|
|
7
|
+
const previous = diagnosticWriteChains.get(filePath) ?? Promise.resolve();
|
|
8
|
+
const next = previous.then(operation, operation);
|
|
9
|
+
diagnosticWriteChains.set(filePath, next.then(() => undefined, () => undefined));
|
|
10
|
+
return next;
|
|
11
|
+
}
|
|
12
|
+
function diagnosticsPath(appData) {
|
|
13
|
+
const repoNamespace = path.dirname(appData.operations);
|
|
14
|
+
return assertSafeAppDataPath(repoNamespace, path.join(repoNamespace, "context-transfer-diagnostics.json"));
|
|
15
|
+
}
|
|
16
|
+
function safeNonNegative(value) {
|
|
17
|
+
return Number.isFinite(value) && (value ?? 0) >= 0 ? Math.round(value ?? 0) : 0;
|
|
18
|
+
}
|
|
19
|
+
function safeReasonCode(value) {
|
|
20
|
+
if (!value)
|
|
21
|
+
return undefined;
|
|
22
|
+
const normalized = value.trim().slice(0, 96);
|
|
23
|
+
return /^[A-Za-z0-9_.-]+$/.test(normalized) ? normalized : "UNCLASSIFIED";
|
|
24
|
+
}
|
|
25
|
+
export async function recordContextTransferDiagnostic(input) {
|
|
26
|
+
const filePath = diagnosticsPath(input.appData);
|
|
27
|
+
return serializeDiagnostic(filePath, async () => {
|
|
28
|
+
const current = (await readJsonIfExists(filePath)) ?? {
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
updatedAt: (input.now ?? new Date()).toISOString(),
|
|
31
|
+
counters: [],
|
|
32
|
+
};
|
|
33
|
+
const dimensions = {
|
|
34
|
+
outcome: input.dimensions.outcome,
|
|
35
|
+
...(input.dimensions.target ? { target: input.dimensions.target } : {}),
|
|
36
|
+
...(input.dimensions.profile ? { profile: input.dimensions.profile } : {}),
|
|
37
|
+
...(input.dimensions.diffKind ? { diffKind: input.dimensions.diffKind } : {}),
|
|
38
|
+
...(safeReasonCode(input.dimensions.reasonCode)
|
|
39
|
+
? { reasonCode: safeReasonCode(input.dimensions.reasonCode) }
|
|
40
|
+
: {}),
|
|
41
|
+
};
|
|
42
|
+
const key = JSON.stringify({ event: input.event, ...dimensions });
|
|
43
|
+
const existing = current.counters.find((counter) => JSON.stringify({ event: counter.event, ...dimensionsFromCounter(counter) }) === key);
|
|
44
|
+
if (existing) {
|
|
45
|
+
existing.count += 1;
|
|
46
|
+
existing.bytes += safeNonNegative(input.dimensions.bytes);
|
|
47
|
+
existing.durationMs += safeNonNegative(input.dimensions.durationMs);
|
|
48
|
+
existing.affectedNodeCount += safeNonNegative(input.dimensions.affectedNodeCount);
|
|
49
|
+
existing.importedNodeCount += safeNonNegative(input.dimensions.importedNodeCount);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
current.counters.push({
|
|
53
|
+
event: input.event,
|
|
54
|
+
...dimensions,
|
|
55
|
+
count: 1,
|
|
56
|
+
bytes: safeNonNegative(input.dimensions.bytes),
|
|
57
|
+
durationMs: safeNonNegative(input.dimensions.durationMs),
|
|
58
|
+
affectedNodeCount: safeNonNegative(input.dimensions.affectedNodeCount),
|
|
59
|
+
importedNodeCount: safeNonNegative(input.dimensions.importedNodeCount),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
current.updatedAt = (input.now ?? new Date()).toISOString();
|
|
63
|
+
await writeSecureJson(filePath, current);
|
|
64
|
+
return current;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function dimensionsFromCounter(counter) {
|
|
68
|
+
return {
|
|
69
|
+
outcome: counter.outcome,
|
|
70
|
+
...(counter.target ? { target: counter.target } : {}),
|
|
71
|
+
...(counter.profile ? { profile: counter.profile } : {}),
|
|
72
|
+
...(counter.diffKind ? { diffKind: counter.diffKind } : {}),
|
|
73
|
+
...(counter.reasonCode ? { reasonCode: counter.reasonCode } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function readContextTransferDiagnostics(appData) {
|
|
77
|
+
return readJsonIfExists(diagnosticsPath(appData));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Append-only lifecycle event log for context-transfer mutations
|
|
81
|
+
* (exec-plan observability: context_export.* / artifact_revision.* /
|
|
82
|
+
* artifact_rerun.*). Events carry ids, counts, bytes, duration and reason
|
|
83
|
+
* codes only — never prompt text, artifact content, secret finding values or
|
|
84
|
+
* host absolute paths. The Console emits only stages it truly observes;
|
|
85
|
+
* CLI-kernel-internal stages (capture, secret scan, adapters) stay in the
|
|
86
|
+
* kernel's own evidence.
|
|
87
|
+
*/
|
|
88
|
+
export const CONTEXT_TRANSFER_EVENT_LOG_LIMIT = 512;
|
|
89
|
+
const CONTEXT_TRANSFER_EVENT_NAME_PATTERN = /^(?:context_export|artifact_revision|artifact_rerun)\.[a-z][a-z_]{0,47}$/;
|
|
90
|
+
const CONTEXT_TRANSFER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
91
|
+
export async function recordContextTransferEvent(input) {
|
|
92
|
+
const at = (input.now ?? new Date()).toISOString();
|
|
93
|
+
if (!CONTEXT_TRANSFER_EVENT_NAME_PATTERN.test(input.event)) {
|
|
94
|
+
return { recorded: false, total: 0 };
|
|
95
|
+
}
|
|
96
|
+
const fields = input.fields ?? {};
|
|
97
|
+
const entry = {
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
at,
|
|
100
|
+
event: input.event,
|
|
101
|
+
...sanitizeLifecycleFields(fields),
|
|
102
|
+
};
|
|
103
|
+
const filePath = lifecycleEventsPath(input.appData);
|
|
104
|
+
return serializeDiagnostic(filePath, async () => {
|
|
105
|
+
await appendLifecycleEvent(filePath, entry);
|
|
106
|
+
const total = await countLifecycleEvents(filePath);
|
|
107
|
+
if (total > CONTEXT_TRANSFER_EVENT_LOG_LIMIT) {
|
|
108
|
+
await trimLifecycleEvents(filePath, CONTEXT_TRANSFER_EVENT_LOG_LIMIT);
|
|
109
|
+
return { recorded: true, total: CONTEXT_TRANSFER_EVENT_LOG_LIMIT };
|
|
110
|
+
}
|
|
111
|
+
return { recorded: true, total };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export async function readContextTransferEvents(appData) {
|
|
115
|
+
const filePath = lifecycleEventsPath(appData);
|
|
116
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
117
|
+
const events = [];
|
|
118
|
+
for (const line of raw.split("\n")) {
|
|
119
|
+
if (!line.trim())
|
|
120
|
+
continue;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(line);
|
|
123
|
+
if (parsed &&
|
|
124
|
+
typeof parsed === "object" &&
|
|
125
|
+
parsed.schemaVersion === 1 &&
|
|
126
|
+
typeof parsed.event === "string") {
|
|
127
|
+
events.push(parsed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// A torn final line after a crash must not break readers.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return events;
|
|
135
|
+
}
|
|
136
|
+
function lifecycleEventsPath(appData) {
|
|
137
|
+
const repoNamespace = path.dirname(appData.operations);
|
|
138
|
+
return assertSafeAppDataPath(repoNamespace, path.join(repoNamespace, "context-transfer-events.jsonl"));
|
|
139
|
+
}
|
|
140
|
+
function safeLifecycleId(value) {
|
|
141
|
+
if (!value)
|
|
142
|
+
return undefined;
|
|
143
|
+
return CONTEXT_TRANSFER_ID_PATTERN.test(value) ? value : undefined;
|
|
144
|
+
}
|
|
145
|
+
function sanitizeLifecycleFields(fields) {
|
|
146
|
+
return {
|
|
147
|
+
...(safeLifecycleId(fields.runId) ? { runId: fields.runId } : {}),
|
|
148
|
+
...(safeLifecycleId(fields.nodeId) ? { nodeId: fields.nodeId } : {}),
|
|
149
|
+
...(safeLifecycleId(fields.exportId) ? { exportId: fields.exportId } : {}),
|
|
150
|
+
...(safeLifecycleId(fields.revisionId)
|
|
151
|
+
? { revisionId: fields.revisionId }
|
|
152
|
+
: {}),
|
|
153
|
+
...(safeLifecycleId(fields.operationId)
|
|
154
|
+
? { operationId: fields.operationId }
|
|
155
|
+
: {}),
|
|
156
|
+
...(fields.profile === "redacted" || fields.profile === "raw"
|
|
157
|
+
? { profile: fields.profile }
|
|
158
|
+
: {}),
|
|
159
|
+
...(fields.target === "generic" ||
|
|
160
|
+
fields.target === "pi-session" ||
|
|
161
|
+
fields.target === "pi-handoff" ||
|
|
162
|
+
fields.target === "readable"
|
|
163
|
+
? { target: fields.target }
|
|
164
|
+
: {}),
|
|
165
|
+
...(fields.bytes !== undefined
|
|
166
|
+
? { bytes: safeNonNegative(fields.bytes) }
|
|
167
|
+
: {}),
|
|
168
|
+
...(fields.durationMs !== undefined
|
|
169
|
+
? { durationMs: safeNonNegative(fields.durationMs) }
|
|
170
|
+
: {}),
|
|
171
|
+
...(safeReasonCode(fields.reasonCode)
|
|
172
|
+
? { reasonCode: safeReasonCode(fields.reasonCode) }
|
|
173
|
+
: {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function appendLifecycleEvent(filePath, entry) {
|
|
177
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
178
|
+
await appendFile(filePath, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
179
|
+
await chmod(filePath, 0o600).catch(() => undefined);
|
|
180
|
+
}
|
|
181
|
+
async function countLifecycleEvents(filePath) {
|
|
182
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
183
|
+
let total = 0;
|
|
184
|
+
for (const line of raw.split("\n")) {
|
|
185
|
+
if (line.trim())
|
|
186
|
+
total += 1;
|
|
187
|
+
}
|
|
188
|
+
return total;
|
|
189
|
+
}
|
|
190
|
+
async function trimLifecycleEvents(filePath, limit) {
|
|
191
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
192
|
+
const lines = raw.split("\n").filter((line) => line.trim());
|
|
193
|
+
const kept = lines.slice(-limit);
|
|
194
|
+
const tmp = `${filePath}.${randomBytes(8).toString("hex")}.tmp`;
|
|
195
|
+
await writeFile(tmp, `${kept.join("\n")}\n`, { mode: 0o600, flag: "wx" });
|
|
196
|
+
await rename(tmp, filePath);
|
|
197
|
+
await chmod(filePath, 0o600).catch(() => undefined);
|
|
198
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "
|
|
3
|
+
import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "../../infrastructure/console/app-data.js";
|
|
4
4
|
import { confirmationPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "./human-gate-token.js";
|
|
5
5
|
export const ALL_CONFIRMATION_CHALLENGES = [
|
|
6
6
|
"source",
|
|
@@ -20,7 +20,7 @@ import { randomBytes } from "node:crypto";
|
|
|
20
20
|
import { readFile } from "node:fs/promises";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
23
|
-
import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "
|
|
23
|
+
import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "../../infrastructure/console/app-data.js";
|
|
24
24
|
import { hashDagBytes, } from "./dag-confirmation.js";
|
|
25
25
|
/**
|
|
26
26
|
* Structured verification command patterns for the G2 autonomous-execution
|