@tea-agent/loop-agent 0.42.0-next.9 → 0.42.1
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 +111 -45
- package/dist/application/dag/run-dag.js +8 -2
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +26 -8
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +2584 -600
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +135 -55
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +173 -94
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +208 -66
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +27 -7
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +21 -0
- package/dist/worker/console/chat/tools.js +13 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -5
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +431 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +51 -4
- package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +64 -22
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +194 -243
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +103 -17
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +21 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +146 -16
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +46 -7
- package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +70 -35
- package/dist/workflows/dag/node-execution.js +225 -226
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +96 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +246 -19
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +68 -4
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/harness.json +1 -1
- package/package.json +8 -6
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +22 -2
- package/skills/frontend-plan/references/decision-contract.md +114 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
2
2
|
import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
|
|
3
|
+
import { isToolFailure } from "../../chat/tool-preview.js";
|
|
3
4
|
import { mergeOperationCardState, } from "../../chat/operation-card.js";
|
|
4
5
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
6
|
+
import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
|
|
5
7
|
import { taskContextUnchanged } from "./format.js";
|
|
6
8
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
9
|
+
import { sameTurnGroupInputs } from "./turn-group-equality.js";
|
|
7
10
|
const EMPTY_USAGE = {
|
|
8
11
|
inputTokens: 0,
|
|
9
12
|
outputTokens: 0,
|
|
@@ -114,10 +117,20 @@ export function useChatThread(params) {
|
|
|
114
117
|
// (UI-08); the SSE applier must not open the process panel.
|
|
115
118
|
void params.setProcessOpen;
|
|
116
119
|
const [messages, setMessages] = useState([]);
|
|
120
|
+
/** Rendered-messages mirror for the replay flush: it must resolve its target
|
|
121
|
+
* row synchronously, before React commits the queued updater (2026-09-08). */
|
|
122
|
+
const messagesRef = useRef([]);
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
messagesRef.current = messages;
|
|
125
|
+
}, [messages]);
|
|
117
126
|
const [toolCalls, setToolCalls] = useState([]);
|
|
118
127
|
const [operationCards, setOperationCards] = useState([]);
|
|
119
128
|
const [artifactCards, setArtifactCards] = useState([]);
|
|
120
129
|
const [historyLimit, setHistoryLimit] = useState(50);
|
|
130
|
+
const [historyHasMore, setHistoryHasMore] = useState(false);
|
|
131
|
+
const [historyNextCursor, setHistoryNextCursor] = useState(null);
|
|
132
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
133
|
+
const historyLoadingRef = useRef(false);
|
|
121
134
|
const [unread, setUnread] = useState(0);
|
|
122
135
|
const [humanGateCards, setHumanGateCards] = useState([]);
|
|
123
136
|
const [interview, setInterview] = useState(null);
|
|
@@ -136,6 +149,8 @@ export function useChatThread(params) {
|
|
|
136
149
|
/** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
|
|
137
150
|
const [streamingAssistantId, setStreamingAssistantId] = useState(null);
|
|
138
151
|
const [showSystemInMessages] = useState(false);
|
|
152
|
+
const completedGroupsRef = useRef([]);
|
|
153
|
+
const previousTimelineRef = useRef(null);
|
|
139
154
|
const refreshSessionTelemetry = useCallback(async () => {
|
|
140
155
|
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
141
156
|
if (!targetSessionId)
|
|
@@ -180,6 +195,7 @@ export function useChatThread(params) {
|
|
|
180
195
|
onTelemetryRefresh: () => void refreshSessionTelemetry(),
|
|
181
196
|
setMessages,
|
|
182
197
|
setToolCalls,
|
|
198
|
+
getMessages: () => messagesRef.current,
|
|
183
199
|
}), [refs, onReconcile, setError, refreshSessionTelemetry]);
|
|
184
200
|
/** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
|
|
185
201
|
* eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
|
|
@@ -278,7 +294,7 @@ export function useChatThread(params) {
|
|
|
278
294
|
if (toolCalls.length === 0)
|
|
279
295
|
return null;
|
|
280
296
|
const running = toolCalls.filter((t) => t.status === "running");
|
|
281
|
-
const errored = toolCalls.filter((t) => t
|
|
297
|
+
const errored = toolCalls.filter((t) => isToolFailure(t));
|
|
282
298
|
const current = running[running.length - 1];
|
|
283
299
|
let statusLabel = "完成";
|
|
284
300
|
let statusKind = "ok";
|
|
@@ -301,12 +317,57 @@ export function useChatThread(params) {
|
|
|
301
317
|
};
|
|
302
318
|
}, [toolCalls]);
|
|
303
319
|
const turnGroups = useMemo(() => {
|
|
304
|
-
const
|
|
320
|
+
const previousTimeline = previousTimelineRef.current;
|
|
321
|
+
let built;
|
|
322
|
+
const activeIndex = streamingAssistantId
|
|
323
|
+
? timelineMessages.findIndex((message) => message.id === streamingAssistantId)
|
|
324
|
+
: -1;
|
|
325
|
+
const canBuildActiveTail = Boolean(previousTimeline && streaming && streamingAssistantId && activeIndex >= 0) &&
|
|
326
|
+
previousTimeline?.activeAssistantId === streamingAssistantId &&
|
|
327
|
+
(previousTimeline?.messages.every((message, index) => message.id === streamingAssistantId || timelineMessages[index]?.id === message.id) ?? false);
|
|
328
|
+
if (canBuildActiveTail && previousTimeline) {
|
|
329
|
+
let tailStart = activeIndex;
|
|
330
|
+
while (tailStart > 0 && timelineMessages[tailStart - 1]?.role !== "user")
|
|
331
|
+
tailStart -= 1;
|
|
332
|
+
const tailMessages = timelineMessages.slice(tailStart);
|
|
333
|
+
const tailIds = new Set(tailMessages.map((message) => message.id));
|
|
334
|
+
const tailTurnIds = new Set(tailMessages.map((message) => message.turnId).filter(Boolean));
|
|
335
|
+
const tailTools = toolCalls.filter((tool) => tailIds.has(tool.assistantMessageId ?? "") ||
|
|
336
|
+
(Boolean(tool.turnId) && tailTurnIds.has(tool.turnId)) ||
|
|
337
|
+
(tool.createdAt >= (tailMessages[0]?.createdAt ?? Number.POSITIVE_INFINITY)));
|
|
338
|
+
const activeTail = buildTurnProcessGroups({
|
|
339
|
+
messages: tailMessages,
|
|
340
|
+
toolCalls: tailTools,
|
|
341
|
+
streaming,
|
|
342
|
+
activeAssistantId: streamingAssistantId,
|
|
343
|
+
});
|
|
344
|
+
built = [...previousTimeline.groups.filter((group) => !group.live), ...activeTail];
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
built = buildTurnProcessGroups({
|
|
348
|
+
messages: timelineMessages,
|
|
349
|
+
toolCalls,
|
|
350
|
+
streaming,
|
|
351
|
+
activeAssistantId: streamingAssistantId,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// Preserve completed group objects across streaming tokens. The active
|
|
355
|
+
// tail is rebuilt, while stable historical groups keep their references so
|
|
356
|
+
// memoized rows do not re-render or re-parse the full conversation.
|
|
357
|
+
const previous = new Map(completedGroupsRef.current.map((group) => [group.key, group]));
|
|
358
|
+
const all = built.map((group) => {
|
|
359
|
+
const cached = previous.get(group.key);
|
|
360
|
+
return cached && !group.live && sameTurnGroupInputs(cached, group)
|
|
361
|
+
? cached
|
|
362
|
+
: group;
|
|
363
|
+
});
|
|
364
|
+
completedGroupsRef.current = all.filter((group) => !group.live);
|
|
365
|
+
previousTimelineRef.current = {
|
|
305
366
|
messages: timelineMessages,
|
|
306
367
|
toolCalls,
|
|
307
|
-
streaming,
|
|
308
368
|
activeAssistantId: streamingAssistantId,
|
|
309
|
-
|
|
369
|
+
groups: all,
|
|
370
|
+
};
|
|
310
371
|
return selectRecentTurnGroups(all, historyLimit);
|
|
311
372
|
}, [
|
|
312
373
|
timelineMessages,
|
|
@@ -315,6 +376,50 @@ export function useChatThread(params) {
|
|
|
315
376
|
streamingAssistantId,
|
|
316
377
|
historyLimit,
|
|
317
378
|
]);
|
|
379
|
+
const loadEarlierMessages = useCallback(async () => {
|
|
380
|
+
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
381
|
+
const beforeId = historyNextCursor;
|
|
382
|
+
if (!targetSessionId || !historyHasMore || !beforeId || historyLoadingRef.current)
|
|
383
|
+
return false;
|
|
384
|
+
historyLoadingRef.current = true;
|
|
385
|
+
setHistoryLoading(true);
|
|
386
|
+
try {
|
|
387
|
+
const query = new URLSearchParams({ limit: "50", beforeId });
|
|
388
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}/messages?${query.toString()}`, { credentials: "include" });
|
|
389
|
+
if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
390
|
+
return false;
|
|
391
|
+
const body = (await res.json());
|
|
392
|
+
const page = hydrateSessionThread(body.messages ?? []);
|
|
393
|
+
setMessages((current) => {
|
|
394
|
+
const seen = new Set();
|
|
395
|
+
const merged = [...page.messages, ...current].filter((message) => {
|
|
396
|
+
if (seen.has(message.id))
|
|
397
|
+
return false;
|
|
398
|
+
seen.add(message.id);
|
|
399
|
+
return true;
|
|
400
|
+
});
|
|
401
|
+
return merged.sort((a, b) => {
|
|
402
|
+
if (a.eventSeq !== undefined && b.eventSeq !== undefined && a.eventSeq !== b.eventSeq)
|
|
403
|
+
return a.eventSeq - b.eventSeq;
|
|
404
|
+
return a.createdAt - b.createdAt;
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
setToolCalls((current) => {
|
|
408
|
+
const byId = new Map(current.map((tool) => [tool.id, tool]));
|
|
409
|
+
for (const tool of page.toolCalls)
|
|
410
|
+
byId.set(tool.id, tool);
|
|
411
|
+
return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt);
|
|
412
|
+
});
|
|
413
|
+
setHistoryHasMore(body.hasMore === true);
|
|
414
|
+
setHistoryNextCursor(body.nextCursor ?? null);
|
|
415
|
+
setHistoryLimit((value) => value + 50);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
historyLoadingRef.current = false;
|
|
420
|
+
setHistoryLoading(false);
|
|
421
|
+
}
|
|
422
|
+
}, [historyHasMore, historyNextCursor, origin, refs]);
|
|
318
423
|
const visibleMessages = useMemo(() => {
|
|
319
424
|
const next = [];
|
|
320
425
|
for (const group of turnGroups) {
|
|
@@ -346,12 +451,41 @@ export function useChatThread(params) {
|
|
|
346
451
|
setStreamingAssistantId(null);
|
|
347
452
|
setUnread(0);
|
|
348
453
|
setHistoryLimit(50);
|
|
454
|
+
setHistoryHasMore(false);
|
|
455
|
+
setHistoryNextCursor(null);
|
|
456
|
+
historyLoadingRef.current = false;
|
|
457
|
+
completedGroupsRef.current = [];
|
|
458
|
+
previousTimelineRef.current = null;
|
|
349
459
|
refs.usageResponseKeysRef.current = new Set();
|
|
350
460
|
}, [refs]);
|
|
351
461
|
/** Adopt a freshly activated session's hydrated thread + streaming flags. */
|
|
352
462
|
const applyActivatedSession = useCallback((next) => {
|
|
353
463
|
const visuallyStopped = next.turnPhase === "stopping";
|
|
354
|
-
|
|
464
|
+
// A submission still owned for THIS session keeps its optimistic bubble
|
|
465
|
+
// across an activation snapshot (refresh / reopen); a lease owned by
|
|
466
|
+
// another session must never leak into this thread (2026-09-08).
|
|
467
|
+
const ownedPending = refs.pendingSubmissionRef.current?.sessionId ===
|
|
468
|
+
refs.sessionRef.current?.sessionId
|
|
469
|
+
? refs.pendingSubmissionRef.current
|
|
470
|
+
: undefined;
|
|
471
|
+
setMessages(mergePendingSubmissionOnHydration(next.messages, ownedPending
|
|
472
|
+
? {
|
|
473
|
+
clientRequestId: ownedPending.clientRequestId,
|
|
474
|
+
user: {
|
|
475
|
+
id: ownedPending.userMessageId,
|
|
476
|
+
role: "user",
|
|
477
|
+
text: ownedPending.text,
|
|
478
|
+
createdAt: Date.now(),
|
|
479
|
+
clientRequestId: ownedPending.clientRequestId,
|
|
480
|
+
},
|
|
481
|
+
assistant: {
|
|
482
|
+
id: ownedPending.assistantId,
|
|
483
|
+
role: "assistant",
|
|
484
|
+
text: "",
|
|
485
|
+
createdAt: Date.now(),
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
: undefined));
|
|
355
489
|
setToolCalls(next.toolCalls);
|
|
356
490
|
setOperationCards([]);
|
|
357
491
|
setHumanGateCards([]);
|
|
@@ -374,6 +508,10 @@ export function useChatThread(params) {
|
|
|
374
508
|
setStreamingAssistantId(null);
|
|
375
509
|
setUnread(0);
|
|
376
510
|
setHistoryLimit(50);
|
|
511
|
+
setHistoryHasMore(next.history?.hasMore === true);
|
|
512
|
+
setHistoryNextCursor(next.history?.nextCursor ?? null);
|
|
513
|
+
completedGroupsRef.current = [];
|
|
514
|
+
previousTimelineRef.current = null;
|
|
377
515
|
}, [refs]);
|
|
378
516
|
/** Reconcile recovery (AC-001/AC-002/AC-003): reload the canonical session
|
|
379
517
|
* facts (session-store messages + event-ring tail cursor) and REPLACE the
|
|
@@ -415,8 +553,29 @@ export function useChatThread(params) {
|
|
|
415
553
|
refs.turnAssistantIdsRef.current = new Map();
|
|
416
554
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
417
555
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
418
|
-
|
|
556
|
+
const pending = refs.pendingSubmissionRef.current;
|
|
557
|
+
const recovered = mergePendingSubmissionOnHydration(hydrated.messages, pending
|
|
558
|
+
? {
|
|
559
|
+
clientRequestId: pending.clientRequestId,
|
|
560
|
+
user: {
|
|
561
|
+
id: pending.userMessageId,
|
|
562
|
+
role: "user",
|
|
563
|
+
text: pending.text,
|
|
564
|
+
createdAt: Date.now(),
|
|
565
|
+
clientRequestId: pending.clientRequestId,
|
|
566
|
+
},
|
|
567
|
+
assistant: {
|
|
568
|
+
id: pending.assistantId,
|
|
569
|
+
role: "assistant",
|
|
570
|
+
text: "",
|
|
571
|
+
createdAt: Date.now(),
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
: undefined);
|
|
575
|
+
setMessages(mergeCompactionMessages(recovered, body.compactions));
|
|
419
576
|
setToolCalls(hydrated.toolCalls);
|
|
577
|
+
setHistoryHasMore(body.history?.hasMore === true);
|
|
578
|
+
setHistoryNextCursor(body.history?.nextCursor ?? null);
|
|
420
579
|
setStreamingAssistantId(null);
|
|
421
580
|
return true;
|
|
422
581
|
}
|
|
@@ -446,6 +605,10 @@ export function useChatThread(params) {
|
|
|
446
605
|
artifactCards,
|
|
447
606
|
historyLimit,
|
|
448
607
|
setHistoryLimit,
|
|
608
|
+
historyHasMore,
|
|
609
|
+
historyNextCursor,
|
|
610
|
+
historyLoading,
|
|
611
|
+
loadEarlierMessages,
|
|
449
612
|
unread,
|
|
450
613
|
humanGateCards,
|
|
451
614
|
interview,
|
|
@@ -7,7 +7,7 @@ import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
|
|
|
7
7
|
/** Composer state: input text, @file palette, image attachments, /shortcuts,
|
|
8
8
|
* IME composition guards, auto-grow and server-side draft sync. */
|
|
9
9
|
export function useComposer(params) {
|
|
10
|
-
const { origin, sessionId, refs, setError, onUnrecoverableSession } = params;
|
|
10
|
+
const { origin, sessionId, sessionMode, refs, setError, onUnrecoverableSession, } = params;
|
|
11
11
|
const [input, setInputState] = useState("");
|
|
12
12
|
const [fileOptions, setFileOptions] = useState([]);
|
|
13
13
|
const [pendingImages, setPendingImages] = useState([]);
|
|
@@ -28,7 +28,10 @@ export function useComposer(params) {
|
|
|
28
28
|
}, []);
|
|
29
29
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
30
30
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
31
|
-
const [commandCapabilities, setCommandCapabilities] = useState({
|
|
31
|
+
const [commandCapabilities, setCommandCapabilities] = useState({
|
|
32
|
+
sddBingo: false,
|
|
33
|
+
sessionMode: undefined,
|
|
34
|
+
});
|
|
32
35
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
33
36
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
34
37
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
@@ -75,10 +78,10 @@ export function useComposer(params) {
|
|
|
75
78
|
commandsSessionRef.current = sessionId;
|
|
76
79
|
commandsGenerationRef.current += 1;
|
|
77
80
|
setDynamicCommands([]);
|
|
78
|
-
setCommandCapabilities({ sddBingo: false });
|
|
81
|
+
setCommandCapabilities({ sddBingo: false, sessionMode });
|
|
79
82
|
setDynamicCommandsError(null);
|
|
80
83
|
setDynamicCommandsLoading(false);
|
|
81
|
-
}, [sessionId]);
|
|
84
|
+
}, [sessionId, sessionMode]);
|
|
82
85
|
const loadDynamicCommands = useCallback(async () => {
|
|
83
86
|
if (!sessionId) {
|
|
84
87
|
setDynamicCommands([]);
|
|
@@ -124,7 +127,10 @@ export function useComposer(params) {
|
|
|
124
127
|
if (!isCurrent())
|
|
125
128
|
return;
|
|
126
129
|
setDynamicCommands(items);
|
|
127
|
-
setCommandCapabilities({
|
|
130
|
+
setCommandCapabilities({
|
|
131
|
+
sddBingo: body?.capabilities?.sddBingo === true,
|
|
132
|
+
sessionMode: body?.capabilities?.sessionMode ?? sessionMode,
|
|
133
|
+
});
|
|
128
134
|
// Compact server fail-soft / partial-source warning; local Operator/Web still merge.
|
|
129
135
|
const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
|
|
130
136
|
setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
|
|
@@ -134,14 +140,14 @@ export function useComposer(params) {
|
|
|
134
140
|
return;
|
|
135
141
|
setDynamicCommandsError("动态命令加载失败");
|
|
136
142
|
setDynamicCommands([]);
|
|
137
|
-
setCommandCapabilities({ sddBingo: false });
|
|
143
|
+
setCommandCapabilities({ sddBingo: false, sessionMode });
|
|
138
144
|
}
|
|
139
145
|
finally {
|
|
140
146
|
if (isCurrent()) {
|
|
141
147
|
setDynamicCommandsLoading(false);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
|
-
}, [origin, sessionId]);
|
|
150
|
+
}, [origin, sessionId, sessionMode]);
|
|
145
151
|
// Lazy-load dynamic commands when slash palette opens or session changes.
|
|
146
152
|
useEffect(() => {
|
|
147
153
|
if (!sessionId)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** User anchors only depend on the ordered user-message objects, not on
|
|
2
|
+
* assistant/process updates that rebuild their containing turn groups. */
|
|
3
|
+
export function sameUserMessages(previous, next) {
|
|
4
|
+
let previousIndex = 0;
|
|
5
|
+
let nextIndex = 0;
|
|
6
|
+
for (;;) {
|
|
7
|
+
while (previousIndex < previous.length && !previous[previousIndex].user) {
|
|
8
|
+
previousIndex += 1;
|
|
9
|
+
}
|
|
10
|
+
while (nextIndex < next.length && !next[nextIndex].user) {
|
|
11
|
+
nextIndex += 1;
|
|
12
|
+
}
|
|
13
|
+
if (previousIndex === previous.length || nextIndex === next.length) {
|
|
14
|
+
return previousIndex === previous.length && nextIndex === next.length;
|
|
15
|
+
}
|
|
16
|
+
if (previous[previousIndex].user !== next[nextIndex].user)
|
|
17
|
+
return false;
|
|
18
|
+
previousIndex += 1;
|
|
19
|
+
nextIndex += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -170,6 +170,10 @@ export function withWorkspaceId(href, workspaceId) {
|
|
|
170
170
|
const qs = query.toString();
|
|
171
171
|
return `/#${path}${qs ? `?${qs}` : ""}`;
|
|
172
172
|
}
|
|
173
|
+
/** The primary Observe mode is a section entry, not an object deep link. */
|
|
174
|
+
export function buildPrimaryInspectHref(workspaceId) {
|
|
175
|
+
return withWorkspaceId(DEFAULT_INSPECT_HREF, workspaceId);
|
|
176
|
+
}
|
|
173
177
|
/** Preserve the selected scope for a deep link emitted inside an Operate page.
|
|
174
178
|
* Components should prefer this to hand-writing `/#/inspect/...`: it reads the
|
|
175
179
|
* canonical hash at click/render time and intentionally leaves bare Chat URLs
|
|
@@ -37,6 +37,8 @@ import { UserQuestionRegistry } from "./chat/user-questions.js";
|
|
|
37
37
|
import { TodoListRegistry } from "./chat/todos.js";
|
|
38
38
|
import { GoalRegistry } from "./chat/goals.js";
|
|
39
39
|
import { GoalRoundDriver } from "./chat/goal-round-driver.js";
|
|
40
|
+
import { TerminalSessionRegistry } from "./chat/terminal-sessions.js";
|
|
41
|
+
import { ScmService } from "./chat/scm-service.js";
|
|
40
42
|
import { driveGoalRound, recoverDeferredTurns } from "./chat/routes.js";
|
|
41
43
|
import { createOperationEventStore, } from "./operation-sse.js";
|
|
42
44
|
import { OperationStore } from "../../infrastructure/console/operation-store.js";
|
|
@@ -254,7 +256,9 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
254
256
|
}
|
|
255
257
|
},
|
|
256
258
|
});
|
|
257
|
-
const
|
|
259
|
+
const aimaxAuth = new AimaxAuthReader();
|
|
260
|
+
await aimaxAuth.getToken();
|
|
261
|
+
const sddDataAlignment = new SddDataAlignmentService(chatStore, aimaxAuth, deps.fetchImpl);
|
|
258
262
|
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
259
263
|
const chatEvents = createChatEventStore({
|
|
260
264
|
appData,
|
|
@@ -285,6 +289,12 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
285
289
|
operations,
|
|
286
290
|
operationEvents: events,
|
|
287
291
|
});
|
|
292
|
+
// Persistent PTY terminal sessions shared by the model tool (terminal) and
|
|
293
|
+
// the Console UI terminal panel (HTTP API), keyed per chat session owner.
|
|
294
|
+
const terminalRegistry = new TerminalSessionRegistry();
|
|
295
|
+
// Workspace-scoped git source control shared by the model tool (scm) and
|
|
296
|
+
// the Console UI SCM panel (HTTP API).
|
|
297
|
+
const scmService = new ScmService(repoRoot);
|
|
288
298
|
const chatRuntime = new ConsolePiRuntime({
|
|
289
299
|
cwd: repoRoot,
|
|
290
300
|
sessionDir: appData.chatSessions,
|
|
@@ -294,6 +304,8 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
294
304
|
todoRegistry,
|
|
295
305
|
goalRegistry,
|
|
296
306
|
sddDataAlignment,
|
|
307
|
+
terminalRegistry,
|
|
308
|
+
scmService,
|
|
297
309
|
});
|
|
298
310
|
let chatRouteDeps;
|
|
299
311
|
const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
|
|
@@ -308,6 +320,8 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
308
320
|
sddDataAlignment,
|
|
309
321
|
goalRoundDriver,
|
|
310
322
|
operationLinker: chatOperationLinker,
|
|
323
|
+
terminals: terminalRegistry,
|
|
324
|
+
scm: scmService,
|
|
311
325
|
appData,
|
|
312
326
|
bootToken: deps.bootToken,
|
|
313
327
|
getConsoleOrigin: deps.getConsoleOrigin,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
5
|
-
import { effectiveNodeContextV1Schema, } from "../../shared/context-transfer.js";
|
|
5
|
+
import { redactSecrets, truncateUtf8Preview, redactSensitivePathsWithFindings } from "../../shared/preview.js";
|
|
6
|
+
import { effectiveNodeContextV1Schema, computeContextDigest, } from "../../shared/context-transfer.js";
|
|
6
7
|
import { buildArtifactBindingGraph, } from "../../workflows/dag/artifact-bindings.js";
|
|
7
8
|
import { dagSpecSchema } from "../../workflows/dag/types.js";
|
|
8
9
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
@@ -133,8 +134,7 @@ export async function loadDagNodeContextSummary(repoRoot, dagRunId, nodeId, atte
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
else {
|
|
136
|
-
const
|
|
137
|
-
const parsedReceipt = effectiveNodeContextV1Schema.safeParse(rawReceipt);
|
|
137
|
+
const parsedReceipt = await readValidatedContextReceipt(loaded.facts.runDir, receiptResolved.result.absolutePath);
|
|
138
138
|
if (!parsedReceipt.success ||
|
|
139
139
|
parsedReceipt.data.identity.runId !== dagRunId ||
|
|
140
140
|
parsedReceipt.data.identity.nodeId !== nodeId ||
|
|
@@ -160,6 +160,14 @@ export async function loadDagNodeContextSummary(repoRoot, dagRunId, nodeId, atte
|
|
|
160
160
|
contextDigest: receipt.contextDigest,
|
|
161
161
|
capturedAt: receipt.identity.capturedAt,
|
|
162
162
|
componentCount: receipt.orderedComponents.length,
|
|
163
|
+
components: receipt.orderedComponents.map((component) => ({
|
|
164
|
+
index: component.index, kind: component.kind,
|
|
165
|
+
logicalName: redactSensitivePathsWithFindings(redactSecrets(component.logicalName)).redacted,
|
|
166
|
+
sourceIdentity: redactSensitivePathsWithFindings(redactSecrets(component.source.identity)).redacted,
|
|
167
|
+
inclusion: component.inclusion, bytes: component.bytes, mediaType: component.mediaType,
|
|
168
|
+
previewAvailable: Boolean(component.materializedPath && !["reference-only", "omitted"].includes(component.inclusion)),
|
|
169
|
+
...(component.omissionReason ? { omissionReason: redactSecrets(component.omissionReason) } : {}),
|
|
170
|
+
})),
|
|
163
171
|
componentKinds,
|
|
164
172
|
inclusionCounts,
|
|
165
173
|
truncationCount: receipt.promptAssembly.truncations.length,
|
|
@@ -353,11 +361,7 @@ function isTextPreviewMediaType(mediaType) {
|
|
|
353
361
|
"application/sql",
|
|
354
362
|
].includes(normalized) || normalized.endsWith("+json") || normalized.endsWith("+xml");
|
|
355
363
|
}
|
|
356
|
-
function
|
|
357
|
-
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
358
|
-
return candidate.startsWith(normalizedRoot);
|
|
359
|
-
}
|
|
360
|
-
export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, artifactId) {
|
|
364
|
+
async function loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId) {
|
|
361
365
|
if (!isSafeObservabilityIdentifier(nodeId) || !isSafeObservabilityIdentifier(artifactId)) {
|
|
362
366
|
return {
|
|
363
367
|
status: 400,
|
|
@@ -410,21 +414,7 @@ export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, art
|
|
|
410
414
|
if (!resolved.ok)
|
|
411
415
|
return failureForResolution(resolved.reason);
|
|
412
416
|
try {
|
|
413
|
-
const
|
|
414
|
-
realpath(resolved.result.runDir),
|
|
415
|
-
realpath(resolved.result.absolutePath),
|
|
416
|
-
lstat(resolved.result.absolutePath),
|
|
417
|
-
]);
|
|
418
|
-
if (!isInsideDirectory(runRoot, artifactRealPath) ||
|
|
419
|
-
!fileStat.isFile() ||
|
|
420
|
-
fileStat.isSymbolicLink() ||
|
|
421
|
-
fileStat.nlink !== 1) {
|
|
422
|
-
return {
|
|
423
|
-
status: 422,
|
|
424
|
-
body: { error: "Artifact is not a safe regular file", reason: "unsafe-path" },
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
const bytes = await readFile(artifactRealPath);
|
|
417
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: resolved.result.runDir, targetPath: resolved.result.absolutePath, label: "recorded artifact" });
|
|
428
418
|
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
429
419
|
if (actualSha256 !== fact.sha256 ||
|
|
430
420
|
(typeof fact.bytes === "number" && fact.bytes !== bytes.byteLength)) {
|
|
@@ -436,57 +426,76 @@ export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, art
|
|
|
436
426
|
},
|
|
437
427
|
};
|
|
438
428
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
body: {
|
|
443
|
-
schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
|
|
444
|
-
dagRunId,
|
|
445
|
-
nodeId,
|
|
446
|
-
producerNodeId: artifact.producerNodeId,
|
|
447
|
-
artifactId,
|
|
448
|
-
path: artifact.path,
|
|
449
|
-
mediaType: artifact.mediaType,
|
|
450
|
-
kind: artifact.kind,
|
|
451
|
-
sha256: actualSha256,
|
|
452
|
-
sourceBytes: bytes.byteLength,
|
|
453
|
-
maxBytes: DAG_ARTIFACT_PREVIEW_MAX_BYTES,
|
|
454
|
-
previewKind: "unsupported",
|
|
455
|
-
content: null,
|
|
456
|
-
truncated: false,
|
|
457
|
-
redacted: false,
|
|
458
|
-
},
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
const raw = bytes.toString("utf8");
|
|
462
|
-
const redacted = redactSecrets(raw);
|
|
463
|
-
const content = truncateUtf8Preview(redacted, DAG_ARTIFACT_PREVIEW_MAX_BYTES);
|
|
429
|
+
return { status: 200, artifact, bytes, sha256: actualSha256 };
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
464
432
|
return {
|
|
465
|
-
status:
|
|
466
|
-
body: {
|
|
467
|
-
schemaVersion: NODE_TRANSPARENCY_SCHEMA_VERSION,
|
|
468
|
-
dagRunId,
|
|
469
|
-
nodeId,
|
|
470
|
-
producerNodeId: artifact.producerNodeId,
|
|
471
|
-
artifactId,
|
|
472
|
-
path: artifact.path,
|
|
473
|
-
mediaType: artifact.mediaType,
|
|
474
|
-
kind: artifact.kind,
|
|
475
|
-
sha256: actualSha256,
|
|
476
|
-
sourceBytes: bytes.byteLength,
|
|
477
|
-
maxBytes: DAG_ARTIFACT_PREVIEW_MAX_BYTES,
|
|
478
|
-
previewKind: "text",
|
|
479
|
-
content,
|
|
480
|
-
truncated: content !== redacted,
|
|
481
|
-
redacted: redacted !== raw,
|
|
482
|
-
},
|
|
433
|
+
status: error.code === "ENOENT" ? 404 : 422,
|
|
434
|
+
body: { error: "Artifact file is unavailable or unsafe", reason: error.code === "ENOENT" ? "not-found" : "unsafe-path" },
|
|
483
435
|
};
|
|
484
436
|
}
|
|
437
|
+
}
|
|
438
|
+
export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, artifactId) {
|
|
439
|
+
const verified = await loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId);
|
|
440
|
+
if (verified.status !== 200)
|
|
441
|
+
return verified;
|
|
442
|
+
const { artifact, bytes, sha256 } = verified;
|
|
443
|
+
const supported = isTextPreviewMediaType(artifact.mediaType);
|
|
444
|
+
const raw = supported ? bytes.toString("utf8") : "";
|
|
445
|
+
const safe = redactSecrets(raw);
|
|
446
|
+
const content = supported ? truncateUtf8Preview(safe, DAG_ARTIFACT_PREVIEW_MAX_BYTES) : null;
|
|
447
|
+
return { status: 200, body: { schemaVersion: 1, dagRunId, nodeId, producerNodeId: artifact.producerNodeId, artifactId, path: artifact.path, mediaType: artifact.mediaType, kind: artifact.kind, sha256, sourceBytes: bytes.byteLength, maxBytes: DAG_ARTIFACT_PREVIEW_MAX_BYTES, previewKind: supported ? "text" : "unsupported", content, truncated: supported && content !== safe, redacted: safe !== raw } };
|
|
448
|
+
}
|
|
449
|
+
/** Full text uses the same binding and safety checks as the bounded preview. */
|
|
450
|
+
export async function loadDagNodeArtifactDownload(repoRoot, dagRunId, nodeId, artifactId) {
|
|
451
|
+
const verified = await loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId);
|
|
452
|
+
if (verified.status !== 200)
|
|
453
|
+
return verified;
|
|
454
|
+
if (!isTextPreviewMediaType(verified.artifact.mediaType))
|
|
455
|
+
return { status: 422, body: { error: "Only text artifacts support redacted download", reason: "unsupported-media" } };
|
|
456
|
+
return { status: 200, download: { filename: `${artifactId}.redacted.txt`, bytes: Buffer.from(redactSecrets(verified.bytes.toString("utf8")), "utf8") } };
|
|
457
|
+
}
|
|
458
|
+
async function readValidatedContextReceipt(boundaryRoot, targetPath) {
|
|
459
|
+
try {
|
|
460
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot, targetPath, label: "recorded context receipt" });
|
|
461
|
+
const parsed = effectiveNodeContextV1Schema.safeParse(JSON.parse(bytes.toString("utf8")));
|
|
462
|
+
if (parsed.success) {
|
|
463
|
+
const { contextDigest, ...context } = parsed.data;
|
|
464
|
+
if (computeContextDigest(context) === contextDigest)
|
|
465
|
+
return parsed;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch { /* Missing, changed and unsafe receipts are unavailable. */ }
|
|
469
|
+
return effectiveNodeContextV1Schema.safeParse(null);
|
|
470
|
+
}
|
|
471
|
+
export async function loadDagNodeContextComponent(repoRoot, dagRunId, nodeId, attempt, componentIndex) {
|
|
472
|
+
if (!isSafeObservabilityIdentifier(nodeId) || !Number.isSafeInteger(attempt) || attempt < 1 || !Number.isSafeInteger(componentIndex) || componentIndex < 0)
|
|
473
|
+
return { status: 400, body: { error: "Invalid context selection", reason: "invalid-id" } };
|
|
474
|
+
const loaded = await loadRunFacts(repoRoot, dagRunId);
|
|
475
|
+
if (!loaded.ok)
|
|
476
|
+
return loaded.failure;
|
|
477
|
+
if (!loaded.facts.spec.tasks.some((task) => task.id === nodeId))
|
|
478
|
+
return { status: 404, body: { error: "DAG node not found", reason: "not-found" } };
|
|
479
|
+
const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [nodeId, "context-receipts", `attempt-${attempt}.json`]);
|
|
480
|
+
if (!resolved.ok)
|
|
481
|
+
return failureForResolution(resolved.reason);
|
|
482
|
+
try {
|
|
483
|
+
const parsed = await readValidatedContextReceipt(loaded.facts.runDir, resolved.result.absolutePath);
|
|
484
|
+
if (!parsed.success || parsed.data.identity.runId !== dagRunId || parsed.data.identity.nodeId !== nodeId || parsed.data.identity.attempt !== attempt)
|
|
485
|
+
return { status: 422, body: { error: "Invalid recorded context", reason: "CONTEXT_COMPONENT_UNAVAILABLE" } };
|
|
486
|
+
const component = parsed.data.orderedComponents.find((entry) => entry.index === componentIndex);
|
|
487
|
+
if (!component?.materializedPath || ["reference-only", "omitted"].includes(component.inclusion))
|
|
488
|
+
return { status: 404, body: { error: "No recorded body for this component", reason: "CONTEXT_COMPONENT_UNAVAILABLE" } };
|
|
489
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: loaded.facts.runDir, targetPath: path.resolve(loaded.facts.runDir, ...component.materializedPath.split("/")), label: "recorded context component" });
|
|
490
|
+
if (bytes.byteLength !== component.bytes || createHash("sha256").update(bytes).digest("hex") !== component.sha256)
|
|
491
|
+
return { status: 422, body: { error: "Recorded context component has changed", reason: "CONTEXT_COMPONENT_UNAVAILABLE" } };
|
|
492
|
+
const raw = bytes.toString("utf8");
|
|
493
|
+
const safe = redactSensitivePathsWithFindings(redactSecrets(raw)).redacted;
|
|
494
|
+
const content = truncateUtf8Preview(safe, DAG_ARTIFACT_PREVIEW_MAX_BYTES);
|
|
495
|
+
return { status: 200, body: { dagRunId, nodeId, attempt, componentIndex, sourceBytes: bytes.byteLength, content, redacted: safe !== raw, truncated: content !== safe, maxBytes: DAG_ARTIFACT_PREVIEW_MAX_BYTES } };
|
|
496
|
+
}
|
|
485
497
|
catch {
|
|
486
|
-
return {
|
|
487
|
-
status: 404,
|
|
488
|
-
body: { error: "Artifact file is unavailable", reason: "not-found" },
|
|
489
|
-
};
|
|
498
|
+
return { status: 422, body: { error: "Recorded context is unavailable or unsafe", reason: "CONTEXT_COMPONENT_UNAVAILABLE" } };
|
|
490
499
|
}
|
|
491
500
|
}
|
|
492
501
|
function stringField(value, key) {
|
|
@@ -21,7 +21,7 @@ import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js
|
|
|
21
21
|
import { buildDagNodeInput } from "./node-input.js";
|
|
22
22
|
import { loadDagNodeExecutionOutput } from "./dag-node-execution-output.js";
|
|
23
23
|
import { loadDagNodeProcess } from "./node-process.js";
|
|
24
|
-
import { loadDagNodeArtifactInventory, loadDagNodeArtifactPreview, loadDagNodeContextSummary, loadDagRunLineage, } from "./node-transparency.js";
|
|
24
|
+
import { loadDagNodeArtifactInventory, loadDagNodeArtifactPreview, loadDagNodeArtifactDownload, loadDagNodeContextComponent, loadDagNodeContextSummary, loadDagRunLineage, } from "./node-transparency.js";
|
|
25
25
|
import { buildObserveHealthV1 } from "./health.js";
|
|
26
26
|
import { buildNightJobDetail, buildNightJobsDoctor, buildNightJobsList, buildNightJobsMorning, } from "./night-jobs.js";
|
|
27
27
|
const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
@@ -125,6 +125,8 @@ export const ROUTES = [
|
|
|
125
125
|
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/artifacts$/,
|
|
126
126
|
handler: handleDagNodeArtifacts,
|
|
127
127
|
},
|
|
128
|
+
{ method: "GET", pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/context\/component$/, handler: handleDagNodeContextComponent },
|
|
129
|
+
{ method: "GET", pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/artifacts\/download$/, handler: handleDagNodeArtifactDownload },
|
|
128
130
|
{
|
|
129
131
|
method: "GET",
|
|
130
132
|
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/execution-output$/,
|
|
@@ -795,6 +797,23 @@ async function handleDagNodeContext(_req, res, match, ctx) {
|
|
|
795
797
|
const result = await loadDagNodeContextSummary(ctx.repoRoot, match.params.id, match.params.sub, attempt);
|
|
796
798
|
sendJson(res, result.status, result.body);
|
|
797
799
|
}
|
|
800
|
+
async function handleDagNodeContextComponent(_req, res, match, ctx) {
|
|
801
|
+
if (!/^[1-9]\d*$/.test(match.query.get("attempt") ?? "") || !/^(0|[1-9]\d*)$/.test(match.query.get("componentIndex") ?? "") || match.query.has("path") || match.query.has("artifactPath")) {
|
|
802
|
+
sendJson(res, 400, { error: "Select a recorded attempt and component index" });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const result = await loadDagNodeContextComponent(ctx.repoRoot, match.params.id, match.params.sub, Number(match.query.get("attempt")), Number(match.query.get("componentIndex")));
|
|
806
|
+
sendJson(res, result.status, result.body);
|
|
807
|
+
}
|
|
808
|
+
async function handleDagNodeArtifactDownload(_req, res, match, ctx) {
|
|
809
|
+
const result = await loadDagNodeArtifactDownload(ctx.repoRoot, match.params.id, match.params.sub, match.query.get("artifactId") ?? "");
|
|
810
|
+
if (result.status !== 200) {
|
|
811
|
+
sendJson(res, result.status, result.body);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": `attachment; filename="${result.download.filename}"`, "Content-Length": result.download.bytes.byteLength, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
815
|
+
res.end(result.download.bytes);
|
|
816
|
+
}
|
|
798
817
|
async function handleDagNodeArtifacts(_req, res, match, ctx) {
|
|
799
818
|
const result = await loadDagNodeArtifactInventory(ctx.repoRoot, match.params.id, match.params.sub);
|
|
800
819
|
sendJson(res, result.status, result.body);
|