@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10
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/AGENTS.md +1 -1
- package/CHANGELOG.md +106 -109
- package/README.md +2 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +69 -7
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +43 -3
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- 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/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- 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-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +129 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +312 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +583 -172
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -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 +74 -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 +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +29 -9
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +6 -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/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- 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 +174 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +533 -79
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +6 -4
- 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/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +238 -51
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +62 -12
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-scoped Workspace Context (2026-08-22 multi-workspace foundation, W1).
|
|
3
|
+
*
|
|
4
|
+
* One lazily-created ConsoleContext per workspaceId. Each context INDEPENDENTLY
|
|
5
|
+
* owns the full operator runtime for its repo root: fingerprint-isolated
|
|
6
|
+
* appData (via openConsoleAppData), operation/event stores, draft /
|
|
7
|
+
* confirmation / receipt / interview / assessment stores, Chat session store +
|
|
8
|
+
* events + deferred Turn registry + ConsolePiRuntime(cwd=repoRoot),
|
|
9
|
+
* OperatorActionContext, Observe route context, and readiness.
|
|
10
|
+
*
|
|
11
|
+
* Invariants:
|
|
12
|
+
* - Selecting (viewing) another workspace NEVER disposes a context; running
|
|
13
|
+
* Turns / Operations / DAGs continue in the background.
|
|
14
|
+
* - dispose() is invoked exactly once per context by the owning server's
|
|
15
|
+
* close().
|
|
16
|
+
* - bootToken is shared per Console boot: it binds the startup fingerprint
|
|
17
|
+
* and bind host, and its confirmationToken signs human-gate tokens for all
|
|
18
|
+
* workspaces in this process (single Console security domain).
|
|
19
|
+
*/
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
|
|
24
|
+
import { openConsoleAppData } from "./app-data.js";
|
|
25
|
+
import { DagConfirmationStore } from "./dag-confirmation.js";
|
|
26
|
+
import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
|
|
27
|
+
import { DraftStore } from "./draft-store.js";
|
|
28
|
+
import { AssessmentStore } from "./interview/assessment.js";
|
|
29
|
+
import { InterviewSessionStore } from "./interview/session.js";
|
|
30
|
+
import { ChatSessionStore } from "./chat/session-store.js";
|
|
31
|
+
import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
|
|
32
|
+
import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
|
|
33
|
+
import { ConsolePiRuntime } from "./chat/pi-runtime.js";
|
|
34
|
+
import { UserQuestionRegistry } from "./chat/user-questions.js";
|
|
35
|
+
import { TodoListRegistry } from "./chat/todos.js";
|
|
36
|
+
import { createOperationEventStore, } from "./operation-sse.js";
|
|
37
|
+
import { OperationStore } from "./operation-store.js";
|
|
38
|
+
import { probePiReadiness, } from "./pi-readiness.js";
|
|
39
|
+
import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
40
|
+
import { NightAuxTicker } from "./night-aux-ticker.js";
|
|
41
|
+
import { createObserveRouteContext, } from "../observe/routes.js";
|
|
42
|
+
import { WorkspaceRegistry } from "./workspace-registry.js";
|
|
43
|
+
import { resolveConsoleDefaultTimeoutMs } from "./server.js";
|
|
44
|
+
/**
|
|
45
|
+
* Lazily create the full repo-scoped Console context for a registered
|
|
46
|
+
* workspace record. Pure factory — no global registry; the caller owns
|
|
47
|
+
* lifecycle (single creation per workspaceId, dispose on server close).
|
|
48
|
+
*/
|
|
49
|
+
export async function createWorkspaceContext(record, deps) {
|
|
50
|
+
const repoRoot = record.repoRoot;
|
|
51
|
+
const appData = openConsoleAppData({
|
|
52
|
+
repoRoot,
|
|
53
|
+
...(deps.appDataRoot ? { appDataRoot: deps.appDataRoot } : {}),
|
|
54
|
+
});
|
|
55
|
+
const registry = new WorkspaceRegistry(appData);
|
|
56
|
+
const readiness = await probePiReadiness({
|
|
57
|
+
override: deps.piReadinessProbe,
|
|
58
|
+
repoRoot,
|
|
59
|
+
});
|
|
60
|
+
let readinessCache = readiness;
|
|
61
|
+
const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
|
|
62
|
+
const client = deps.createClient?.(artifactRoot) ??
|
|
63
|
+
new LoopAgentClient({
|
|
64
|
+
loopAgentBin: resolveSiblingLoopAgentBin(),
|
|
65
|
+
artifactRoot,
|
|
66
|
+
resolveIdentity: true,
|
|
67
|
+
defaultTimeoutMs: resolveConsoleDefaultTimeoutMs(deps.defaultTimeoutMs),
|
|
68
|
+
});
|
|
69
|
+
const operations = new OperationStore(appData);
|
|
70
|
+
const events = createOperationEventStore({
|
|
71
|
+
persistenceDir: path.join(appData.operations, "events"),
|
|
72
|
+
});
|
|
73
|
+
const drafts = new DraftStore(appData);
|
|
74
|
+
const confirmations = new DagConfirmationStore(appData, deps.bootToken.confirmationToken);
|
|
75
|
+
const dagExecutionReceipts = new DagExecutionReceiptStore(appData);
|
|
76
|
+
const interviews = new InterviewSessionStore(appData);
|
|
77
|
+
const assessments = new AssessmentStore(appData);
|
|
78
|
+
if (!deps.skipReconcile) {
|
|
79
|
+
await operations.reconcileOnBoot();
|
|
80
|
+
}
|
|
81
|
+
const getReadiness = () => readinessCache;
|
|
82
|
+
const refreshReadiness = async () => {
|
|
83
|
+
readinessCache = await probePiReadiness({
|
|
84
|
+
override: deps.piReadinessProbe,
|
|
85
|
+
repoRoot,
|
|
86
|
+
});
|
|
87
|
+
actionContext.readiness = readinessCache;
|
|
88
|
+
return readinessCache;
|
|
89
|
+
};
|
|
90
|
+
const actionContext = {
|
|
91
|
+
repoRoot,
|
|
92
|
+
appData,
|
|
93
|
+
operatorSessionId: deps.operatorSessionId,
|
|
94
|
+
client,
|
|
95
|
+
operations,
|
|
96
|
+
events,
|
|
97
|
+
drafts,
|
|
98
|
+
confirmations,
|
|
99
|
+
dagExecutionReceipts,
|
|
100
|
+
humanGateSecret: deps.bootToken.confirmationToken,
|
|
101
|
+
interviews,
|
|
102
|
+
assessments,
|
|
103
|
+
readiness: readinessCache,
|
|
104
|
+
getReadiness,
|
|
105
|
+
refreshReadiness,
|
|
106
|
+
observeBaseUrl: deps.observeBaseUrl,
|
|
107
|
+
fetchImpl: deps.fetchImpl,
|
|
108
|
+
getConsoleOrigin: deps.getConsoleOrigin,
|
|
109
|
+
};
|
|
110
|
+
const chatStore = new ChatSessionStore(appData);
|
|
111
|
+
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
112
|
+
const chatEvents = createChatEventStore({
|
|
113
|
+
appData,
|
|
114
|
+
epoch: chatEventEpoch,
|
|
115
|
+
});
|
|
116
|
+
const chatTurnExecutions = createChatTurnExecutionRegistry();
|
|
117
|
+
const questionRegistry = new UserQuestionRegistry(path.join(appData.chats, "user-questions"));
|
|
118
|
+
const todoRegistry = new TodoListRegistry();
|
|
119
|
+
questionRegistry.subscribe((change) => {
|
|
120
|
+
const question = change.question;
|
|
121
|
+
chatEvents.append(question.sessionId, question.turnId ?? `${question.sessionId}:question`, {
|
|
122
|
+
kind: `question_${change.type}`,
|
|
123
|
+
data: { question },
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
todoRegistry.subscribe((change) => {
|
|
127
|
+
chatEvents.append(change.sessionId, change.turnId ?? `${change.sessionId}:todo`, {
|
|
128
|
+
kind: change.type === "updated" ? "todo_updated" : "todo_cleared",
|
|
129
|
+
data: {
|
|
130
|
+
todos: change.todos,
|
|
131
|
+
...(change.toolCallId ? { toolCallId: change.toolCallId } : {}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
const chatOperationLinker = createChatOperationLinker({
|
|
136
|
+
chatEvents,
|
|
137
|
+
operations,
|
|
138
|
+
operationEvents: events,
|
|
139
|
+
});
|
|
140
|
+
const chatRuntime = new ConsolePiRuntime({
|
|
141
|
+
cwd: repoRoot,
|
|
142
|
+
sessionDir: appData.chatSessions,
|
|
143
|
+
resourcePreferencesPath: appData.resourcePreferences,
|
|
144
|
+
actionContext,
|
|
145
|
+
questionRegistry,
|
|
146
|
+
todoRegistry,
|
|
147
|
+
});
|
|
148
|
+
const chatRouteDeps = {
|
|
149
|
+
runtime: chatRuntime,
|
|
150
|
+
store: chatStore,
|
|
151
|
+
events: chatEvents,
|
|
152
|
+
turnExecutions: chatTurnExecutions,
|
|
153
|
+
questionRegistry,
|
|
154
|
+
todoRegistry,
|
|
155
|
+
operationLinker: chatOperationLinker,
|
|
156
|
+
appData,
|
|
157
|
+
bootToken: deps.bootToken,
|
|
158
|
+
getConsoleOrigin: deps.getConsoleOrigin,
|
|
159
|
+
allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
|
|
160
|
+
};
|
|
161
|
+
const { recoverDeferredTurns } = await import("./chat/routes.js");
|
|
162
|
+
const nightAuxTicker = new NightAuxTicker({ repoRoot, client });
|
|
163
|
+
const observeRouteContext = createObserveRouteContext(repoRoot, {
|
|
164
|
+
staticDir: deps.inspectStaticDir,
|
|
165
|
+
});
|
|
166
|
+
let disposed = false;
|
|
167
|
+
const context = {
|
|
168
|
+
workspaceId: record.workspaceId,
|
|
169
|
+
record,
|
|
170
|
+
repoRoot,
|
|
171
|
+
appData,
|
|
172
|
+
registry,
|
|
173
|
+
client,
|
|
174
|
+
operations,
|
|
175
|
+
events,
|
|
176
|
+
drafts,
|
|
177
|
+
confirmations,
|
|
178
|
+
dagExecutionReceipts,
|
|
179
|
+
interviews,
|
|
180
|
+
assessments,
|
|
181
|
+
chatRouteDeps,
|
|
182
|
+
chatOperationLinker,
|
|
183
|
+
chatRuntime,
|
|
184
|
+
nightAuxTicker,
|
|
185
|
+
actionContext,
|
|
186
|
+
observeRouteContext,
|
|
187
|
+
getReadiness,
|
|
188
|
+
refreshReadiness,
|
|
189
|
+
recoverDeferredTurns: () => recoverDeferredTurns(chatRouteDeps),
|
|
190
|
+
dispose: () => {
|
|
191
|
+
if (disposed)
|
|
192
|
+
return;
|
|
193
|
+
disposed = true;
|
|
194
|
+
try {
|
|
195
|
+
chatOperationLinker.dispose();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// best-effort
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
chatRuntime.disposeAll();
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// best-effort
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
nightAuxTicker.dispose();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// best-effort
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
return context;
|
|
215
|
+
}
|
|
216
|
+
/** Inspect-facing readiness/capability summary for a registry record. */
|
|
217
|
+
export async function summarizeWorkspaceRecord(record, deps) {
|
|
218
|
+
let state;
|
|
219
|
+
try {
|
|
220
|
+
const report = await probePiReadiness({
|
|
221
|
+
override: deps.piReadinessProbe,
|
|
222
|
+
repoRoot: record.repoRoot,
|
|
223
|
+
});
|
|
224
|
+
state = report.state;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
state = "setup-required";
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
state,
|
|
231
|
+
summary: `pi=${state}; inspect=${deps.inspectReady ? "ready" : "unavailable"}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
void existsSync;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-level Workspace Registry (2026-08-22 multi-workspace foundation, W1).
|
|
3
|
+
*
|
|
4
|
+
* Persists the set of registered Console workspaces at
|
|
5
|
+
* `<appDataRoot>/workspaces.json` (user level, shared across Console boots),
|
|
6
|
+
* independent of per-repo fingerprint namespaces.
|
|
7
|
+
*
|
|
8
|
+
* Invariants:
|
|
9
|
+
* - workspaceId is stable per normalized realpath: it is derived from
|
|
10
|
+
* sha256("loop-console-workspace-v1\0" + realpath) and never minted twice.
|
|
11
|
+
* - REMOVE is soft (hidden=true); on-disk directories, AppData namespaces,
|
|
12
|
+
* Sessions, Operations and DAGs are never deleted.
|
|
13
|
+
* - Re-adding a previously removed workspace RESTORES the original record
|
|
14
|
+
* (same workspaceId + repoFingerprint; only lastAccessedAt advances).
|
|
15
|
+
* - Duplicate visible realpath is rejected; add failures leave the registry
|
|
16
|
+
* byte-identical (same ids and order).
|
|
17
|
+
*/
|
|
18
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
19
|
+
import { realpathSync, statSync } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { readJsonIfExists, writeSecureJson, } from "./app-data.js";
|
|
22
|
+
import { normalizeWorktreeRealpath, repoFingerprintV1, } from "./repo-fingerprint.js";
|
|
23
|
+
export const WORKSPACE_REGISTRY_SCHEMA_VERSION = 1;
|
|
24
|
+
/** Stable workspaceId: sha256("loop-console-workspace-v1\0" + normalizedRealpath). */
|
|
25
|
+
export function workspaceIdForRealpath(normalizedRealpath) {
|
|
26
|
+
return createHash("sha256")
|
|
27
|
+
.update(`loop-console-workspace-v1\0${normalizedRealpath}`, "utf8")
|
|
28
|
+
.digest("hex");
|
|
29
|
+
}
|
|
30
|
+
export class WorkspaceRegistry {
|
|
31
|
+
appData;
|
|
32
|
+
options;
|
|
33
|
+
file;
|
|
34
|
+
cache;
|
|
35
|
+
constructor(appData, options = {}) {
|
|
36
|
+
this.appData = appData;
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.file = path.join(appData.root, "workspaces.json");
|
|
39
|
+
}
|
|
40
|
+
/** Auto-register the startup repoRoot (idempotent). */
|
|
41
|
+
async ensureStartupWorkspace(repoRoot) {
|
|
42
|
+
const record = await this.add(repoRoot);
|
|
43
|
+
if (record.ok)
|
|
44
|
+
return record.record;
|
|
45
|
+
// Startup root may already be visible (duplicate) or previously hidden —
|
|
46
|
+
// re-adding a hidden record restores it via add(); the only expected
|
|
47
|
+
// failure left is DUPLICATE (already visible), which returns the existing.
|
|
48
|
+
const existing = await this.findByRealpath(repoRoot);
|
|
49
|
+
if (existing)
|
|
50
|
+
return existing;
|
|
51
|
+
throw new Error(`workspace registry: startup root rejected: ${record.code}`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Register (or restore) a workspace directory.
|
|
55
|
+
* Failures never mutate the persisted registry.
|
|
56
|
+
* `options.name` overrides the default directory-name label.
|
|
57
|
+
*/
|
|
58
|
+
async add(rawPath, options = {}) {
|
|
59
|
+
const trimmed = rawPath.trim();
|
|
60
|
+
if (!trimmed) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: "INVALID_PATH",
|
|
64
|
+
message: "path is required",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
let real;
|
|
68
|
+
try {
|
|
69
|
+
real = realpathSync(path.resolve(trimmed));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
code: "NOT_A_DIRECTORY",
|
|
75
|
+
message: `path does not exist: ${trimmed}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let st;
|
|
79
|
+
try {
|
|
80
|
+
st = statSync(real);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
code: "UNREADABLE",
|
|
86
|
+
message: `path is not readable: ${trimmed}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!st.isDirectory()) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
code: "NOT_A_DIRECTORY",
|
|
93
|
+
message: `path is not a directory: ${trimmed}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const normalized = normalizeWorktreeRealpath(real);
|
|
97
|
+
const existing = await this.findByRealpathNormalized(normalized);
|
|
98
|
+
if (existing && !existing.hidden) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
code: "DUPLICATE",
|
|
102
|
+
message: `workspace already registered: ${existing.displayPath}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const nowIso = (this.options.now ?? (() => new Date()))().toISOString();
|
|
106
|
+
if (existing) {
|
|
107
|
+
// Restore the soft-removed record; identity (workspaceId,
|
|
108
|
+
// repoFingerprint, createdAt, order) is preserved.
|
|
109
|
+
existing.hidden = undefined;
|
|
110
|
+
existing.lastAccessedAt = nowIso;
|
|
111
|
+
if (options.name && options.name.trim()) {
|
|
112
|
+
existing.name = options.name.trim();
|
|
113
|
+
}
|
|
114
|
+
const registry = await this.load();
|
|
115
|
+
await this.persist(registry);
|
|
116
|
+
return { ok: true, record: existing, registryUnchanged: false };
|
|
117
|
+
}
|
|
118
|
+
const record = {
|
|
119
|
+
workspaceId: workspaceIdForRealpath(normalized),
|
|
120
|
+
name: options.name?.trim() || path.basename(normalized) || normalized,
|
|
121
|
+
displayPath: normalized,
|
|
122
|
+
repoRoot: real,
|
|
123
|
+
repoFingerprint: repoFingerprintV1(real),
|
|
124
|
+
createdAt: nowIso,
|
|
125
|
+
lastAccessedAt: nowIso,
|
|
126
|
+
};
|
|
127
|
+
const registry = await this.load();
|
|
128
|
+
registry.workspaces.push(record);
|
|
129
|
+
await this.persist(registry);
|
|
130
|
+
return { ok: true, record, registryUnchanged: false };
|
|
131
|
+
}
|
|
132
|
+
/** Visible list in canonical insertion order. */
|
|
133
|
+
async listVisible() {
|
|
134
|
+
const registry = await this.load();
|
|
135
|
+
return registry.workspaces.filter((workspace) => {
|
|
136
|
+
if (workspace.hidden)
|
|
137
|
+
return false;
|
|
138
|
+
try {
|
|
139
|
+
return statSync(workspace.repoRoot).isDirectory();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Keep historical registry data, but do not project stale test roots
|
|
143
|
+
// into the Workspace browser after their directories disappear.
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async get(workspaceId) {
|
|
149
|
+
const registry = await this.load();
|
|
150
|
+
return registry.workspaces.find((w) => w.workspaceId === workspaceId);
|
|
151
|
+
}
|
|
152
|
+
/** Get a VISIBLE workspace by id (removed workspaces are not operable). */
|
|
153
|
+
async getVisible(workspaceId) {
|
|
154
|
+
const record = await this.get(workspaceId);
|
|
155
|
+
if (!record || record.hidden)
|
|
156
|
+
return undefined;
|
|
157
|
+
try {
|
|
158
|
+
return statSync(record.repoRoot).isDirectory() ? record : undefined;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Soft-remove: hides the record; never touches directories or AppData. */
|
|
165
|
+
async remove(workspaceId) {
|
|
166
|
+
const registry = await this.load();
|
|
167
|
+
const record = registry.workspaces.find((w) => w.workspaceId === workspaceId);
|
|
168
|
+
if (!record || record.hidden)
|
|
169
|
+
return false;
|
|
170
|
+
record.hidden = true;
|
|
171
|
+
await this.persist(registry);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
async findByRealpath(rawPath) {
|
|
175
|
+
let normalized;
|
|
176
|
+
try {
|
|
177
|
+
normalized = normalizeWorktreeRealpath(path.resolve(rawPath.trim()));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
return this.findByRealpathNormalized(normalized);
|
|
183
|
+
}
|
|
184
|
+
async findByRealpathNormalized(normalized) {
|
|
185
|
+
const registry = await this.load();
|
|
186
|
+
return registry.workspaces.find((w) => w.displayPath === normalized);
|
|
187
|
+
}
|
|
188
|
+
async load() {
|
|
189
|
+
if (this.cache)
|
|
190
|
+
return this.cache;
|
|
191
|
+
const raw = await readJsonIfExists(this.file).catch(() => undefined);
|
|
192
|
+
const registry = {
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
workspaces: raw && Array.isArray(raw.workspaces)
|
|
195
|
+
? raw.workspaces.filter((w) => Boolean(w) &&
|
|
196
|
+
typeof w === "object" &&
|
|
197
|
+
typeof w.workspaceId === "string" &&
|
|
198
|
+
typeof w.displayPath === "string" &&
|
|
199
|
+
typeof w.repoRoot === "string")
|
|
200
|
+
: [],
|
|
201
|
+
};
|
|
202
|
+
this.cache = registry;
|
|
203
|
+
return registry;
|
|
204
|
+
}
|
|
205
|
+
async persist(registry) {
|
|
206
|
+
// writeSecureJson is atomic; a reader mid-write sees the previous bytes.
|
|
207
|
+
await writeSecureJson(this.file, registry);
|
|
208
|
+
this.cache = registry;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Deterministic-looking nonce reserved for tests requiring unique roots. */
|
|
212
|
+
export function mintWorkspaceNonce() {
|
|
213
|
+
return randomBytes(8).toString("hex");
|
|
214
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildGlobalSnapshot } from "./read-model.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read-only runtime guard shared by Console maintenance and init-surface
|
|
6
|
+
* writes. Worker activity remains owned by the Observe projection so both
|
|
7
|
+
* callers use one canonical liveness interpretation.
|
|
8
|
+
*/
|
|
9
|
+
export async function readInitRuntimeActivity(repoRoot) {
|
|
10
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
11
|
+
const activeDagRoot = path.join(resolvedRoot, ".harness", "dag-runs", "active");
|
|
12
|
+
const entries = await readdir(activeDagRoot, { withFileTypes: true }).catch(() => []);
|
|
13
|
+
const activeDagRunIds = entries
|
|
14
|
+
.filter((entry) => entry.isDirectory() && entry.name !== ".gitkeep")
|
|
15
|
+
.map((entry) => entry.name)
|
|
16
|
+
.sort();
|
|
17
|
+
const snapshot = await buildGlobalSnapshot({ repoRoot: resolvedRoot });
|
|
18
|
+
return {
|
|
19
|
+
activeDagRunIds,
|
|
20
|
+
activeWorkerBatches: snapshot.health.worker.activeBatches,
|
|
21
|
+
activeWorkerTasks: snapshot.health.worker.activeTasks,
|
|
22
|
+
...(snapshot.projectionError
|
|
23
|
+
? { workerProjectionError: snapshot.projectionError.message }
|
|
24
|
+
: {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -31,6 +31,7 @@ function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
|
|
|
31
31
|
summary: null,
|
|
32
32
|
dependency: null,
|
|
33
33
|
prompt: null,
|
|
34
|
+
promptCandidates: [],
|
|
34
35
|
executorInput: null,
|
|
35
36
|
boundaries: null,
|
|
36
37
|
promptFingerprint: null,
|
|
@@ -139,7 +140,7 @@ function resolveModelHint(spec, task) {
|
|
|
139
140
|
}
|
|
140
141
|
return null;
|
|
141
142
|
}
|
|
142
|
-
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings) {
|
|
143
|
+
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, promptCandidates, warnings) {
|
|
143
144
|
const defaults = spec.defaults && typeof spec.defaults === "object"
|
|
144
145
|
? spec.defaults
|
|
145
146
|
: undefined;
|
|
@@ -267,6 +268,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExt
|
|
|
267
268
|
maxBytes: promptBody.maxBytes,
|
|
268
269
|
source: promptSource,
|
|
269
270
|
},
|
|
271
|
+
promptCandidates,
|
|
270
272
|
executorInput: {
|
|
271
273
|
kind,
|
|
272
274
|
skills,
|
|
@@ -292,6 +294,89 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExt
|
|
|
292
294
|
warnings,
|
|
293
295
|
};
|
|
294
296
|
}
|
|
297
|
+
async function loadFailedPromptCandidates(repoRoot, dagRunId, nodeId, warnings) {
|
|
298
|
+
const stateResolved = resolveDagRunArtifact(repoRoot, dagRunId, ["state.json"]);
|
|
299
|
+
if (!stateResolved.ok)
|
|
300
|
+
return [];
|
|
301
|
+
let state;
|
|
302
|
+
try {
|
|
303
|
+
state = JSON.parse(await readFile(stateResolved.result.absolutePath, "utf-8"));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
warnings.push("state.json could not be read for failed prompt candidates.");
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
const nodes = state.nodes && typeof state.nodes === "object" && !Array.isArray(state.nodes)
|
|
310
|
+
? state.nodes
|
|
311
|
+
: null;
|
|
312
|
+
const node = nodes?.[nodeId] &&
|
|
313
|
+
typeof nodes[nodeId] === "object" &&
|
|
314
|
+
!Array.isArray(nodes[nodeId])
|
|
315
|
+
? nodes[nodeId]
|
|
316
|
+
: null;
|
|
317
|
+
if (!node)
|
|
318
|
+
return [];
|
|
319
|
+
const failedAttempts = new Set();
|
|
320
|
+
if (Array.isArray(node.attempts)) {
|
|
321
|
+
for (const entry of node.attempts) {
|
|
322
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
323
|
+
continue;
|
|
324
|
+
const record = entry;
|
|
325
|
+
if (record.ok === false &&
|
|
326
|
+
typeof record.attempt === "number" &&
|
|
327
|
+
Number.isSafeInteger(record.attempt) &&
|
|
328
|
+
record.attempt > 0) {
|
|
329
|
+
failedAttempts.add(record.attempt);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (node.status === "ERROR" &&
|
|
334
|
+
typeof node.currentAttempt === "number" &&
|
|
335
|
+
Number.isSafeInteger(node.currentAttempt) &&
|
|
336
|
+
node.currentAttempt > 0) {
|
|
337
|
+
failedAttempts.add(node.currentAttempt);
|
|
338
|
+
}
|
|
339
|
+
const candidates = [];
|
|
340
|
+
for (const attempt of [...failedAttempts].sort((a, b) => a - b)) {
|
|
341
|
+
const fileName = `prompt-candidate-attempt-${attempt}.json`;
|
|
342
|
+
const artifact = path.posix.join(nodeId, fileName);
|
|
343
|
+
const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [nodeId, fileName]);
|
|
344
|
+
if (!resolved.ok) {
|
|
345
|
+
warnings.push(`Failed prompt candidate missing for attempt ${attempt}.`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const raw = JSON.parse(await readFile(resolved.result.absolutePath, "utf-8"));
|
|
350
|
+
if (raw.schemaVersion !== 1 ||
|
|
351
|
+
raw.attempt !== attempt ||
|
|
352
|
+
typeof raw.failureCategory !== "string" ||
|
|
353
|
+
typeof raw.prompt !== "string" ||
|
|
354
|
+
typeof raw.truncated !== "boolean" ||
|
|
355
|
+
typeof raw.maxBytes !== "number" ||
|
|
356
|
+
!Number.isSafeInteger(raw.maxBytes) ||
|
|
357
|
+
raw.maxBytes < 1 ||
|
|
358
|
+
raw.maxBytes > NODE_INPUT_PREVIEW_MAX_BYTES ||
|
|
359
|
+
typeof raw.sha256 !== "string" ||
|
|
360
|
+
!/^([a-f0-9]{64})$/.test(raw.sha256)) {
|
|
361
|
+
throw new Error("invalid candidate schema");
|
|
362
|
+
}
|
|
363
|
+
const prompt = truncatedField(raw.prompt, raw.maxBytes);
|
|
364
|
+
candidates.push({
|
|
365
|
+
attempt,
|
|
366
|
+
failureCategory: redactSecrets(raw.failureCategory),
|
|
367
|
+
prompt: prompt.text,
|
|
368
|
+
truncated: raw.truncated || prompt.truncated,
|
|
369
|
+
maxBytes: raw.maxBytes,
|
|
370
|
+
sha256Prefix: raw.sha256.slice(0, 12),
|
|
371
|
+
artifact,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
warnings.push(`Failed prompt candidate is invalid for attempt ${attempt}.`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return candidates;
|
|
379
|
+
}
|
|
295
380
|
/**
|
|
296
381
|
* Load run-time Pi extension facts from <node>/pi-extensions.json (plan
|
|
297
382
|
* 2026-08-22 R2). Returns null when the node declares no piExtensions
|
|
@@ -486,8 +571,9 @@ export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
|
|
|
486
571
|
? task.piExtensions.filter((item) => typeof item === "string")
|
|
487
572
|
: [];
|
|
488
573
|
const piExtensionRunFacts = await loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declaredPiExtensions, warnings);
|
|
574
|
+
const promptCandidates = await loadFailedPromptCandidates(repoRoot, dagRunId, nodeId, warnings);
|
|
489
575
|
// Reject unknown nested leakage by projecting only through allowlist helpers.
|
|
490
|
-
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings);
|
|
576
|
+
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, promptCandidates, warnings);
|
|
491
577
|
// Defense-in-depth: ensure raw task never sneaks in.
|
|
492
578
|
if ("task" in body) {
|
|
493
579
|
return {
|