@tea-agent/loop-agent 0.40.0-next.9 → 0.41.0-beta.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/AGENTS.md +1 -1
- package/CHANGELOG.md +130 -3
- package/README.md +2 -2
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +21 -5
- package/dist/cli/program.js +61 -4
- package/dist/commands/client-recovery.js +8 -36
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/commands/init-model-catalog.js +22 -0
- package/dist/commands/task-advance.js +23 -1
- package/dist/commands/task-source-prepare.js +5 -0
- package/dist/commands/task-status.js +1 -0
- package/dist/executors/dag-pi-executor.js +39 -12
- package/dist/executors/pi-sdk-executor.js +36 -5
- package/dist/executors/shell-executor.js +64 -17
- package/dist/governance/checks.js +1 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +12 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +23 -1
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +2 -1
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/assistant-content.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +84 -13
- package/dist/worker/console/chat/goal-round-driver.js +72 -0
- package/dist/worker/console/chat/goals.js +300 -0
- package/dist/worker/console/chat/pi-runtime.js +285 -8
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +487 -30
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +28 -19
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/turn-process.js +32 -3
- package/dist/worker/console/chat/user-questions.js +1 -1
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +21 -3
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +124 -8
- package/dist/worker/console/operator-selection.js +6 -2
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +169 -0
- package/dist/worker/console/pi-plugins.catalog.json +73658 -0
- package/dist/worker/console/pi-plugins.js +179 -0
- package/dist/worker/console/routes.js +864 -13
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +177 -44
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-DSVcg1YO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-BtXhIqui.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-B-nh-ilJ.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BIUSVdZI.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-Cmw3L4qM.js} +1 -1
- package/dist/worker/console/static/assets/channel-BhZFNefx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGpMMjGm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BNBkEpEJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-ubln3eaU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-_XcBTt1i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-Ln30Pgrh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-ESH3lhSx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DAJt-qXB.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-Cx10LIcH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-CENeOxoJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-DXqK-D38.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Sbv-NJni.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Sbv-NJni.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-CE4dmvGY.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CRacN1zK.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cneukjrt.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DPFboRoL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DIAQgcOP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-VrYMKxuy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-CLHvBghn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-DVCWBif8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-BTMmbBzJ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BUZsKJ56.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-BsBZ5NXC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-D7fH_kDa.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-Bza5OohY.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DpkoGR_Y.js} +1 -1
- package/dist/worker/console/static/assets/index-Ius3vxhf.css +1 -0
- package/dist/worker/console/static/assets/index-yASzcbBr.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BYm3g6LT.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C2RS_H4v.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4slKpCo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-B7orRNpI.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-D1BeBxLQ.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-0TtMP8k4.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-XL5EbCtT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CwF7uas5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-A_Jo-72f.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CxOa4gKL.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-DoAoswIH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-C1Lc1MlD.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-DFXOkq0U.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-B0ufUCuW.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-iPbW55g3.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CHiXzEKP.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BtMgzo_H.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-LyFs7TpX.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cb-OVd9Y.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-tNkJnb6z.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-DFm7Oo8o.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-tpblTp9i.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-BCDLWR_D.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
- package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
- package/dist/worker/console/workspace-context.js +56 -5
- package/dist/worker/console/workspace-registry.js +335 -62
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/static/api.js +69 -5
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
- package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
- package/dist/worker/observe/static/inspect-workspace.js +54 -10
- package/dist/worker/observe/static/operator-chrome.css +23 -4
- package/dist/worker/observe/static/operator-chrome.js +7 -2
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1108 -135
- package/dist/worker/observe/static/views/dag-inspector.js +1257 -59
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/worker/observe/static/views/session-timeline.js +210 -10
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
- package/dist/workflows/dag/budget-enforcement.js +10 -0
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +151 -4
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +555 -1
- package/dist/workflows/dag/rerun-run.js +601 -15
- package/dist/workflows/dag/runner.js +20 -1
- package/dist/workflows/dag/skill-snapshot.js +17 -0
- package/dist/workflows/dag/types.js +183 -7
- package/docs/architecture/runtime-boundaries.md +8 -7
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/agent-dag.schema.json +32 -0
- package/docs/templates/backend-test-dag.json +6 -5
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/task.yaml +1 -1
- package/package.json +5 -2
- package/skills/loop-agent/references/command-reference.md +21 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DAG_HISTORY_DEFAULT_PAGE_SIZE,
|
|
16
16
|
navigate,
|
|
17
17
|
parseRoute,
|
|
18
|
+
inspectPathToCanonicalHref,
|
|
18
19
|
} from "../router.js";
|
|
19
20
|
import { fetchDagRunHistory, summarizeFetchError } from "../api.js";
|
|
20
21
|
import { copyText } from "../copy.js";
|
|
@@ -57,7 +58,8 @@ function currentHistoryQuery() {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function syncHash(page, pageSize, { replace = false } = {}) {
|
|
60
|
-
const
|
|
61
|
+
const path = buildDagHistoryHash(page, pageSize);
|
|
62
|
+
const next = inspectPathToCanonicalHref(path).replace(/^\/#/, "") || "/inspect";
|
|
61
63
|
const current = (location.hash || "").replace(/^#/, "") || "/";
|
|
62
64
|
const currentPath = current.split("?")[0];
|
|
63
65
|
const nextPath = next.split("?")[0];
|
|
@@ -72,15 +74,15 @@ function syncHash(page, pageSize, { replace = false } = {}) {
|
|
|
72
74
|
location.hash = next;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
return
|
|
77
|
+
return path;
|
|
76
78
|
}
|
|
77
79
|
if (replace && typeof history !== "undefined" && history.replaceState) {
|
|
78
80
|
const base = `${location.pathname}${location.search}`;
|
|
79
81
|
history.replaceState(null, "", `${base}#${next}`);
|
|
80
|
-
return
|
|
82
|
+
return path;
|
|
81
83
|
}
|
|
82
|
-
navigate(
|
|
83
|
-
return
|
|
84
|
+
navigate(path);
|
|
85
|
+
return path;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
function setControlsDisabled(disabled) {
|
|
@@ -30,6 +30,112 @@ export function sessionEventArgs(event) {
|
|
|
30
30
|
return args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function escapeHtml(text) {
|
|
34
|
+
return String(text ?? "")
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """)
|
|
39
|
+
.replaceAll("'", "'");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function downloadTextFile(filename, text) {
|
|
43
|
+
const blob = new Blob([text], { type: "text/html;charset=utf-8" });
|
|
44
|
+
const url = URL.createObjectURL(blob);
|
|
45
|
+
const a = document.createElement("a");
|
|
46
|
+
a.href = url;
|
|
47
|
+
a.download = filename;
|
|
48
|
+
document.body.appendChild(a);
|
|
49
|
+
a.click();
|
|
50
|
+
a.remove();
|
|
51
|
+
URL.revokeObjectURL(url);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 兜底样式:仅在无法抓取页面 /inspect/styles.css 时使用(与页面 process-timeline 排版对齐)。 */
|
|
55
|
+
const EXPORT_FALLBACK_CSS = `
|
|
56
|
+
:root{--canvas:#f7f7f4;--surface:#ffffff;--canvas-soft:#fafaf7;--surface-muted:#f0efe9;--ink:#26251e;--body:#5a5852;--muted:#807d72;--muted-soft:#a09c92;--hairline:#e6e5e0;--hairline-strong:#cfcdc4;--orange:#f54e00;--orange-active:#d04200;--blue:#3f6ea4;--green:#1f8a65;--amber:#a66a00;--red:#c72d53;--lavender:#e4daf1;--radius-sm:6px;--space-2:8px;--space-3:12px;--space-5:20px}
|
|
57
|
+
body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas);color:var(--body);font-size:13px}
|
|
58
|
+
.process-timeline{display:grid;gap:0;margin:0;padding:var(--space-2) 0;list-style:none}
|
|
59
|
+
.process-timeline-item{display:grid;grid-template-columns:20px minmax(0,1fr);align-items:stretch;min-height:34px}
|
|
60
|
+
.process-timeline-item.has-time{grid-template-columns:20px 92px minmax(0,1fr)}
|
|
61
|
+
.process-timeline-rail{position:relative;display:grid;place-items:center;min-height:34px}
|
|
62
|
+
.process-timeline-rail::before{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline-strong);content:""}
|
|
63
|
+
.process-timeline-item.is-first .process-timeline-rail::before{top:50%}
|
|
64
|
+
.process-timeline-item.is-last .process-timeline-rail::before{bottom:50%}
|
|
65
|
+
.process-timeline-dot{position:relative;z-index:1;width:8px;height:8px;border:2px solid var(--surface);border-radius:50%;background:var(--muted-soft)}
|
|
66
|
+
.process-timeline-lifecycle .process-timeline-dot{background:var(--lavender)}
|
|
67
|
+
.process-timeline-tool-start .process-timeline-dot{background:var(--orange)}
|
|
68
|
+
.process-timeline-tool-end .process-timeline-dot{background:var(--green)}
|
|
69
|
+
.process-timeline-assistant .process-timeline-dot{background:var(--blue)}
|
|
70
|
+
.process-timeline-protocol .process-timeline-dot{background:var(--muted-soft)}
|
|
71
|
+
.process-timeline-item.is-error .process-timeline-dot{background:var(--red)}
|
|
72
|
+
.process-timeline-time{padding:9px 0;color:var(--muted);font-size:10px;font-variant-numeric:tabular-nums}
|
|
73
|
+
.process-timeline-label{min-width:0;padding:8px 0 8px var(--space-2);color:var(--body);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
|
|
74
|
+
.process-timeline-protocol .process-timeline-label{color:var(--muted);font-size:11px}
|
|
75
|
+
.process-timeline-body{min-width:0;padding:4px 0 8px var(--space-2)}
|
|
76
|
+
.process-timeline-card{display:grid;gap:6px;min-width:0}
|
|
77
|
+
.process-timeline-details{min-width:0;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface)}
|
|
78
|
+
.process-timeline-summary{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;cursor:pointer;list-style:none;color:var(--ink);font-size:12px;font-weight:650}
|
|
79
|
+
.process-timeline-summary::-webkit-details-marker{display:none}
|
|
80
|
+
.process-timeline-summary-title{min-width:0;overflow-wrap:anywhere}
|
|
81
|
+
.process-timeline-summary-status{flex-shrink:0;color:var(--muted);font-size:11px;font-weight:700}
|
|
82
|
+
.process-timeline-summary-status.status-succeeded{color:var(--green)}
|
|
83
|
+
.process-timeline-summary-status.status-failed{color:var(--red)}
|
|
84
|
+
.process-timeline-summary-status.status-pending{color:var(--orange)}
|
|
85
|
+
.process-timeline-tool-card.is-error .process-timeline-details{border-color:var(--red);background:var(--canvas-soft)}
|
|
86
|
+
.process-timeline-card-body{display:grid;gap:6px;padding:0 10px 10px}
|
|
87
|
+
.process-timeline-section-label{color:var(--muted);font-size:11px;font-weight:700}
|
|
88
|
+
.process-timeline-code,.process-timeline-thinking-body{margin:0;padding:8px 10px;overflow:auto;border-radius:var(--radius-sm);background:var(--canvas-soft);color:var(--body);font-family:"JetBrains Mono","SFMono-Regular",Consolas,monospace;font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
89
|
+
.process-timeline-more{margin-top:2px}
|
|
90
|
+
.process-timeline-more>summary{cursor:pointer;color:var(--blue);font-size:11px;font-weight:650}
|
|
91
|
+
.process-timeline-thinking>summary{background:var(--lavender)}
|
|
92
|
+
.process-timeline-turn-text{display:grid;gap:6px;padding:8px 10px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface)}
|
|
93
|
+
.process-timeline-turn-text .markdown-output{font-size:12px}
|
|
94
|
+
.process-timeline-tool-only-note{margin:0;font-size:12px}
|
|
95
|
+
.process-timeline-turn-tools{display:grid;gap:4px;margin:0;padding:0 0 0 1.1em;color:var(--body);font-size:12px;font-family:"JetBrains Mono","SFMono-Regular",Consolas,monospace;overflow-wrap:anywhere}
|
|
96
|
+
.process-timeline-turn-tools li{padding:2px 0}
|
|
97
|
+
.process-timeline-agent-end-note{margin:0;font-size:11px}
|
|
98
|
+
.markdown-output{display:grid;gap:var(--space-3);color:var(--body);font-size:13px;line-height:1.65}
|
|
99
|
+
.markdown-output>:first-child{margin-top:0}
|
|
100
|
+
.markdown-output>:last-child{margin-bottom:0}
|
|
101
|
+
.markdown-output h1,.markdown-output h2,.markdown-output h3,.markdown-output h4{margin:var(--space-5) 0 var(--space-2);color:var(--ink);line-height:1.3}
|
|
102
|
+
.markdown-output h1{font-size:21px}.markdown-output h2{font-size:18px}.markdown-output h3{font-size:16px}.markdown-output h4{font-size:14px}
|
|
103
|
+
.markdown-output p,.markdown-output blockquote,.markdown-output ul,.markdown-output ol{margin:0;overflow-wrap:anywhere}
|
|
104
|
+
.markdown-output ul,.markdown-output ol{padding-left:22px}
|
|
105
|
+
.markdown-output blockquote{padding-left:12px}
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const EXPORT_SHELL_CSS = `
|
|
109
|
+
body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas)}
|
|
110
|
+
.export-shell{max-width:1000px;margin:0 auto;padding:24px 28px;background:var(--surface);min-height:100vh;box-shadow:0 0 0 1px var(--hairline)}
|
|
111
|
+
.export-head h1{font-size:15px;color:var(--ink);margin:0;font-weight:700}
|
|
112
|
+
.export-sub{color:var(--muted);font-size:12px;margin:4px 0 16px}
|
|
113
|
+
.export-controls{display:flex;justify-content:flex-start;margin:0 0 12px}
|
|
114
|
+
.export-toggle{min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--body);font-size:11px;font-weight:600;cursor:pointer;user-select:none}
|
|
115
|
+
.export-toggle:hover,.export-toggle:focus-visible{border-color:var(--orange);color:var(--orange-active);outline:none}
|
|
116
|
+
body.hide-protocol .process-timeline-item.process-timeline-protocol{display:none}
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 把页面渲染出的时间线 innerHTML 封装成自包含 HTML。
|
|
121
|
+
* @param {string} bodyHTML 页面渲染的 .process-timeline 等真实 DOM 序列化结果
|
|
122
|
+
* @param {{nodeId?:string,dagRunId?:string}} meta
|
|
123
|
+
* @param {string} [cssText] 页面 /inspect/styles.css 原文;缺失时用兜底样式
|
|
124
|
+
*/
|
|
125
|
+
export function buildPageStyledDoc(bodyHTML, meta = {}, cssText = "") {
|
|
126
|
+
const now = new Date().toLocaleString();
|
|
127
|
+
const title = meta.nodeId ? `节点执行过程 · ${meta.nodeId}` : "节点执行过程";
|
|
128
|
+
const style = cssText.trim()
|
|
129
|
+
? `${cssText}\n\n/* 导出外壳 */\n${EXPORT_SHELL_CSS}`
|
|
130
|
+
: `${EXPORT_FALLBACK_CSS}\n${EXPORT_SHELL_CSS}`;
|
|
131
|
+
// 有协议行时,在左上角提供一个「展开/隐藏原始协议事件」按钮(与页面一致),默认隐藏。
|
|
132
|
+
const hasProtocol = bodyHTML.includes("process-timeline-protocol");
|
|
133
|
+
const controls = hasProtocol
|
|
134
|
+
? `<div class="export-controls"><button type="button" id="exportToggleProtocol" class="export-toggle">展开原始协议事件</button></div><script>(function(){var b=document.body,t=document.getElementById('exportToggleProtocol'),on=false;function apply(){b.classList.toggle('hide-protocol',!on);t.textContent=on?'隐藏原始协议事件':'展开原始协议事件';}if(t){t.addEventListener('click',function(){on=!on;apply();});apply();}})();</script>`
|
|
135
|
+
: "";
|
|
136
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${style}</style></head><body><div class="export-shell"><div class="export-head"><h1>${escapeHtml(title)}</h1></div><p class="export-sub">DAG ${escapeHtml(meta.dagRunId ?? "")} · 导出于 ${escapeHtml(now)}</p>${controls}<div class="export-timeline">${bodyHTML}</div></div></body></html>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
33
139
|
export function sessionEventResult(event) {
|
|
34
140
|
return normalizeSessionEventFields(event).result;
|
|
35
141
|
}
|
|
@@ -480,6 +586,35 @@ export function buildRichTimelineItems(events, options = {}) {
|
|
|
480
586
|
return items;
|
|
481
587
|
}
|
|
482
588
|
|
|
589
|
+
const EXECUTION_RECORD_FILTERS = Object.freeze([
|
|
590
|
+
["all", "全部"],
|
|
591
|
+
["model", "模型消息"],
|
|
592
|
+
["tools", "工具与进程"],
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
function normalizeExecutionRecordFilter(value) {
|
|
596
|
+
return value === "model" || value === "tools" ? value : "all";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function filterExecutionRecordItems(items, filter) {
|
|
600
|
+
const normalized = normalizeExecutionRecordFilter(filter);
|
|
601
|
+
if (normalized === "all") return items;
|
|
602
|
+
return items.filter((item) => {
|
|
603
|
+
const protocolEvents = Array.isArray(item.protocolEvents)
|
|
604
|
+
? item.protocolEvents
|
|
605
|
+
: [item.event];
|
|
606
|
+
const toolProtocol =
|
|
607
|
+
item.kind === "protocol" &&
|
|
608
|
+
protocolEvents.some((event) => {
|
|
609
|
+
const role = event?.message?.role ?? event?.role;
|
|
610
|
+
return role === "tool" || role === "toolResult";
|
|
611
|
+
});
|
|
612
|
+
const modelFact =
|
|
613
|
+
item.kind === "turn" || (item.kind === "protocol" && !toolProtocol);
|
|
614
|
+
return normalized === "model" ? modelFact : !modelFact;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
483
618
|
function ensureOpenDetailsSet() {
|
|
484
619
|
if (!(uiState.sessionTimelineOpenDetails instanceof Set)) {
|
|
485
620
|
uiState.sessionTimelineOpenDetails = new Set();
|
|
@@ -723,7 +858,26 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
723
858
|
}
|
|
724
859
|
|
|
725
860
|
const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
|
|
861
|
+
const activeFilter = normalizeExecutionRecordFilter(
|
|
862
|
+
uiState.executionRecordFilter,
|
|
863
|
+
);
|
|
726
864
|
const toolbar = el("div", "process-timeline-toolbar");
|
|
865
|
+
const filterGroup = el("div", "process-timeline-filter-group");
|
|
866
|
+
filterGroup.setAttribute("role", "group");
|
|
867
|
+
filterGroup.setAttribute("aria-label", "筛选执行记录");
|
|
868
|
+
for (const [filter, label] of EXECUTION_RECORD_FILTERS) {
|
|
869
|
+
const button = document.createElement("button");
|
|
870
|
+
button.type = "button";
|
|
871
|
+
button.className = "process-timeline-toggle process-timeline-filter";
|
|
872
|
+
button.setAttribute("aria-pressed", String(activeFilter === filter));
|
|
873
|
+
button.textContent = label;
|
|
874
|
+
button.addEventListener("click", () => {
|
|
875
|
+
uiState.executionRecordFilter = filter;
|
|
876
|
+
rerenderSessionTimeline(content, nodeId, options);
|
|
877
|
+
});
|
|
878
|
+
filterGroup.appendChild(button);
|
|
879
|
+
}
|
|
880
|
+
toolbar.appendChild(filterGroup);
|
|
727
881
|
const protocolToggle = document.createElement("button");
|
|
728
882
|
protocolToggle.type = "button";
|
|
729
883
|
protocolToggle.className = "process-timeline-toggle";
|
|
@@ -733,20 +887,44 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
733
887
|
: "展开原始协议事件";
|
|
734
888
|
protocolToggle.addEventListener("click", () => {
|
|
735
889
|
uiState.sessionTimelineShowProtocol = !showProtocol;
|
|
736
|
-
|
|
737
|
-
const scrollTop = parent.scrollTop;
|
|
738
|
-
const scrollLeft = parent.scrollLeft;
|
|
739
|
-
while (parent.firstChild) parent.removeChild(parent.firstChild);
|
|
740
|
-
renderSessionTimeline(parent, nodeId, options);
|
|
741
|
-
parent.scrollTop = scrollTop;
|
|
742
|
-
parent.scrollLeft = scrollLeft;
|
|
890
|
+
rerenderSessionTimeline(content, nodeId, options);
|
|
743
891
|
});
|
|
744
892
|
toolbar.appendChild(protocolToggle);
|
|
893
|
+
const exportBtn = el("button", "btn btn-secondary", "导出 HTML");
|
|
894
|
+
exportBtn.type = "button";
|
|
895
|
+
exportBtn.addEventListener("click", async () => {
|
|
896
|
+
let cssText = "";
|
|
897
|
+
try {
|
|
898
|
+
const res = await fetch("/inspect/styles.css", { cache: "no-cache" });
|
|
899
|
+
if (res.ok) cssText = await res.text();
|
|
900
|
+
} catch {
|
|
901
|
+
/* 忽略:导出仍会用内联兜底样式 */
|
|
902
|
+
}
|
|
903
|
+
// 复用页面真实渲染(含原始协议事件),保证导出的排版与页面一致。
|
|
904
|
+
const prevShow = uiState.sessionTimelineShowProtocol;
|
|
905
|
+
uiState.sessionTimelineShowProtocol = true;
|
|
906
|
+
const tmp = document.createElement("div");
|
|
907
|
+
tmp.className = "process-timeline-scroll";
|
|
908
|
+
renderSessionTimeline(tmp, nodeId, options);
|
|
909
|
+
const toolbar = tmp.querySelector(".process-timeline-toolbar");
|
|
910
|
+
if (toolbar) toolbar.remove();
|
|
911
|
+
const innerHTML = tmp.innerHTML;
|
|
912
|
+
uiState.sessionTimelineShowProtocol = prevShow;
|
|
913
|
+
downloadTextFile(
|
|
914
|
+
`node-process-${nodeId || "node"}-${Date.now()}.html`,
|
|
915
|
+
buildPageStyledDoc(innerHTML, {
|
|
916
|
+
nodeId,
|
|
917
|
+
dagRunId: uiState.currentDagRunId ?? "",
|
|
918
|
+
}, cssText),
|
|
919
|
+
);
|
|
920
|
+
});
|
|
921
|
+
toolbar.appendChild(exportBtn);
|
|
745
922
|
content.appendChild(toolbar);
|
|
746
923
|
|
|
747
|
-
const timelineItems =
|
|
748
|
-
showProtocol,
|
|
749
|
-
|
|
924
|
+
const timelineItems = filterExecutionRecordItems(
|
|
925
|
+
buildRichTimelineItems(visible.events, { showProtocol }),
|
|
926
|
+
activeFilter,
|
|
927
|
+
);
|
|
750
928
|
const hasEventTimes = visible.events.some((event) =>
|
|
751
929
|
Boolean(sessionEventTimestamp(event)),
|
|
752
930
|
);
|
|
@@ -760,6 +938,13 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
760
938
|
);
|
|
761
939
|
}
|
|
762
940
|
|
|
941
|
+
if (timelineItems.length === 0) {
|
|
942
|
+
content.appendChild(
|
|
943
|
+
el("p", "empty", "当前筛选下没有可显示的执行记录。"),
|
|
944
|
+
);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
763
948
|
const ul = el("ul", "process-timeline process-timeline-rich");
|
|
764
949
|
for (let index = 0; index < timelineItems.length; index += 1) {
|
|
765
950
|
const item = timelineItems[index];
|
|
@@ -838,3 +1023,18 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
|
|
|
838
1023
|
content.scrollTop = timelineState.scrollTop;
|
|
839
1024
|
}
|
|
840
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
function rerenderSessionTimeline(content, nodeId, options) {
|
|
1028
|
+
const scrollTop = content.scrollTop;
|
|
1029
|
+
const scrollLeft = content.scrollLeft;
|
|
1030
|
+
const preserved = [...content.children].filter((child) =>
|
|
1031
|
+
String(child.className ?? "")
|
|
1032
|
+
.split(/\s+/)
|
|
1033
|
+
.includes("dag-inspector-attempt-bar"),
|
|
1034
|
+
);
|
|
1035
|
+
while (content.firstChild) content.removeChild(content.firstChild);
|
|
1036
|
+
for (const child of preserved) content.appendChild(child);
|
|
1037
|
+
renderSessionTimeline(content, nodeId, options);
|
|
1038
|
+
content.scrollTop = scrollTop;
|
|
1039
|
+
content.scrollLeft = scrollLeft;
|
|
1040
|
+
}
|
|
@@ -98,6 +98,24 @@ class FrontendImplementationAdapter {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* `backend-test-analysis` — read-only requirements/API analysis. It produces
|
|
103
|
+
* the strict Backend Test Analysis v2 contract without claiming that tests or
|
|
104
|
+
* a real integration ran.
|
|
105
|
+
*/
|
|
106
|
+
class BackendTestAnalysisAdapter {
|
|
107
|
+
workflow = "backend-test-analysis";
|
|
108
|
+
project(input) {
|
|
109
|
+
return {
|
|
110
|
+
artifacts: [
|
|
111
|
+
...canonicalArtifacts(input),
|
|
112
|
+
...structuredArtifactsFromReport(input.dagReportRun),
|
|
113
|
+
],
|
|
114
|
+
acceptanceCoverage: [...input.acceptanceRefs],
|
|
115
|
+
integrationStatus: { mock: false, real: false },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
101
119
|
/**
|
|
102
120
|
* `backend-test` — generates + executes backend tests. Integration status is
|
|
103
121
|
* `real: true` only when the report completed (a real test execution); mock is
|
|
@@ -139,6 +157,7 @@ class FrontendTestAdapter {
|
|
|
139
157
|
const ADAPTERS = new Map([
|
|
140
158
|
["agent-dag", new AgentDagAdapter()],
|
|
141
159
|
["frontend-implementation", new FrontendImplementationAdapter()],
|
|
160
|
+
["backend-test-analysis", new BackendTestAnalysisAdapter()],
|
|
142
161
|
["backend-test", new BackendTestAdapter()],
|
|
143
162
|
["frontend-test", new FrontendTestAdapter()],
|
|
144
163
|
]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { acquireDirLock } from "../../infrastructure/console/dir-lock.js";
|
|
4
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
6
|
import { assertSafeTaskPoolId, getTaskPoolRoot, } from "./run-store.js";
|
|
6
7
|
import { TASK_POOL_STATE_ERROR_CODES, TaskPoolStateError } from "./types.js";
|
|
@@ -19,6 +20,20 @@ export function getAttemptLeasePath(repoRoot, featureId, taskId) {
|
|
|
19
20
|
}
|
|
20
21
|
return leasePath;
|
|
21
22
|
}
|
|
23
|
+
function getAttemptLeaseMutationLockPath(repoRoot, featureId, taskId) {
|
|
24
|
+
assertSafeTaskPoolId(featureId, "featureId");
|
|
25
|
+
assertSafeTaskPoolId(taskId, "taskId");
|
|
26
|
+
return path.join(getAttemptLeasesRoot(repoRoot), "_locks", `${featureId}__${taskId}.lock`);
|
|
27
|
+
}
|
|
28
|
+
async function withAttemptLeaseMutationLock(repoRoot, featureId, taskId, operation) {
|
|
29
|
+
const lock = await acquireDirLock(getAttemptLeaseMutationLockPath(repoRoot, featureId, taskId));
|
|
30
|
+
try {
|
|
31
|
+
return await operation();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await lock.release();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
22
37
|
function isLeaseRecord(value) {
|
|
23
38
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
24
39
|
return false;
|
|
@@ -30,7 +45,11 @@ function isLeaseRecord(value) {
|
|
|
30
45
|
typeof record.batchRunId === "string" &&
|
|
31
46
|
typeof record.token === "string" &&
|
|
32
47
|
typeof record.acquiredAt === "string" &&
|
|
33
|
-
typeof record.renewedAt === "string"
|
|
48
|
+
typeof record.renewedAt === "string" &&
|
|
49
|
+
(record.revisionId === undefined || typeof record.revisionId === "string") &&
|
|
50
|
+
(record.controllerAnchor === undefined ||
|
|
51
|
+
typeof record.controllerAnchor === "string") &&
|
|
52
|
+
(record.claimId === undefined || typeof record.claimId === "string"));
|
|
34
53
|
}
|
|
35
54
|
export async function readAttemptLease(repoRoot, featureId, taskId) {
|
|
36
55
|
const leasePath = getAttemptLeasePath(repoRoot, featureId, taskId);
|
|
@@ -47,82 +66,94 @@ export async function readAttemptLease(repoRoot, featureId, taskId) {
|
|
|
47
66
|
}
|
|
48
67
|
/**
|
|
49
68
|
* Acquire Task-scoped attempt lease before beginWorkerAttempt.
|
|
50
|
-
* Same identity+
|
|
69
|
+
* Same identity+claim reuse is idempotent; a competing admission or active
|
|
70
|
+
* identity conflicts. The Task-scoped mutation lock closes cross-process
|
|
71
|
+
* read-before-rename and release-vs-successor races.
|
|
51
72
|
*/
|
|
52
73
|
export async function acquireAttemptLease(input) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (existing
|
|
57
|
-
existing.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
return withAttemptLeaseMutationLock(input.repoRoot, input.featureId, input.taskId, async () => {
|
|
75
|
+
const now = input.now ?? new Date();
|
|
76
|
+
const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
|
|
77
|
+
if (existing) {
|
|
78
|
+
if (existing.workerRunId === input.workerRunId &&
|
|
79
|
+
existing.batchRunId === input.batchRunId &&
|
|
80
|
+
existing.featureId === input.featureId &&
|
|
81
|
+
existing.taskId === input.taskId &&
|
|
82
|
+
existing.revisionId === input.revisionId &&
|
|
83
|
+
existing.controllerAnchor === input.controllerAnchor &&
|
|
84
|
+
existing.claimId === input.claimId) {
|
|
85
|
+
return { status: "idempotent-reuse", lease: existing };
|
|
86
|
+
}
|
|
87
|
+
throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict for ${input.featureId}/${input.taskId}: held by ${existing.workerRunId}`);
|
|
61
88
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
? {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
const lease = {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
featureId: input.featureId,
|
|
92
|
+
taskId: input.taskId,
|
|
93
|
+
workerRunId: input.workerRunId,
|
|
94
|
+
batchRunId: input.batchRunId,
|
|
95
|
+
acquiredAt: now.toISOString(),
|
|
96
|
+
renewedAt: now.toISOString(),
|
|
97
|
+
token: `atk-${randomBytes(8).toString("hex")}`,
|
|
98
|
+
ownerProcess: { pid: process.pid },
|
|
99
|
+
...(input.revisionId ? { revisionId: input.revisionId } : {}),
|
|
100
|
+
...(input.controllerAnchor
|
|
101
|
+
? { controllerAnchor: input.controllerAnchor }
|
|
102
|
+
: {}),
|
|
103
|
+
...(input.claimId ? { claimId: input.claimId } : {}),
|
|
104
|
+
};
|
|
105
|
+
await mkdir(path.dirname(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId)), {
|
|
106
|
+
recursive: true,
|
|
107
|
+
});
|
|
108
|
+
await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), lease, { repoRoot: input.repoRoot });
|
|
109
|
+
const confirmed = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
|
|
110
|
+
if (!confirmed) {
|
|
111
|
+
throw new Error(`failed to persist attempt lease for ${input.featureId}/${input.taskId}`);
|
|
112
|
+
}
|
|
113
|
+
if (confirmed.token !== lease.token) {
|
|
114
|
+
throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict race for ${input.featureId}/${input.taskId}`);
|
|
115
|
+
}
|
|
116
|
+
return { status: "acquired", lease: confirmed };
|
|
81
117
|
});
|
|
82
|
-
await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), lease, { repoRoot: input.repoRoot });
|
|
83
|
-
const confirmed = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
|
|
84
|
-
if (!confirmed) {
|
|
85
|
-
throw new Error(`failed to persist attempt lease for ${input.featureId}/${input.taskId}`);
|
|
86
|
-
}
|
|
87
|
-
if (confirmed.workerRunId !== input.workerRunId) {
|
|
88
|
-
throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict race for ${input.featureId}/${input.taskId}`);
|
|
89
|
-
}
|
|
90
|
-
return { status: "acquired", lease: confirmed };
|
|
91
118
|
}
|
|
92
119
|
export async function renewAttemptLease(input) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
return withAttemptLeaseMutationLock(input.repoRoot, input.featureId, input.taskId, async () => {
|
|
121
|
+
const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
|
|
122
|
+
if (!existing) {
|
|
123
|
+
throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot renew missing lease for ${input.featureId}/${input.taskId}`);
|
|
124
|
+
}
|
|
125
|
+
if (existing.token !== input.token || existing.workerRunId !== input.workerRunId) {
|
|
126
|
+
throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot renew lease: CAS token/identity mismatch for ${input.featureId}/${input.taskId}`);
|
|
127
|
+
}
|
|
128
|
+
const renewed = {
|
|
129
|
+
...existing,
|
|
130
|
+
renewedAt: (input.now ?? new Date()).toISOString(),
|
|
131
|
+
};
|
|
132
|
+
await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), renewed, { repoRoot: input.repoRoot });
|
|
133
|
+
return renewed;
|
|
134
|
+
});
|
|
106
135
|
}
|
|
107
136
|
/**
|
|
108
137
|
* Release active lease after terminal/recovery evidence. Moves to lease-history then deletes active locator.
|
|
109
138
|
*/
|
|
110
139
|
export async function releaseAttemptLease(input) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
await withAttemptLeaseMutationLock(input.repoRoot, input.featureId, input.taskId, async () => {
|
|
141
|
+
const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
|
|
142
|
+
if (!existing)
|
|
143
|
+
return;
|
|
144
|
+
if (existing.token !== input.token || existing.workerRunId !== input.workerRunId) {
|
|
145
|
+
throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot release lease: CAS token/identity mismatch for ${input.featureId}/${input.taskId}`);
|
|
146
|
+
}
|
|
147
|
+
const historyRoot = path.join(getAttemptLeasesRoot(input.repoRoot), "_history");
|
|
148
|
+
await mkdir(historyRoot, { recursive: true });
|
|
149
|
+
const historyPath = path.join(historyRoot, `${input.featureId}__${input.taskId}__${existing.workerRunId}.json`);
|
|
150
|
+
const activePath = getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId);
|
|
151
|
+
await writeJsonAtomic(historyPath, {
|
|
152
|
+
...existing,
|
|
153
|
+
releasedAt: (input.now ?? new Date()).toISOString(),
|
|
154
|
+
}, { repoRoot: input.repoRoot });
|
|
155
|
+
await unlink(activePath).catch(() => { });
|
|
156
|
+
});
|
|
126
157
|
}
|
|
127
158
|
export function decideAttemptLeaseReclaim(input) {
|
|
128
159
|
if (input.hasTerminalEvidence) {
|
|
@@ -19,6 +19,7 @@ export async function beginWorkerAttemptWithLease(input) {
|
|
|
19
19
|
...(input.controllerAnchor
|
|
20
20
|
? { controllerAnchor: input.controllerAnchor }
|
|
21
21
|
: {}),
|
|
22
|
+
...(input.leaseClaimId ? { claimId: input.leaseClaimId } : {}),
|
|
22
23
|
...(input.now ? { now: input.now } : {}),
|
|
23
24
|
});
|
|
24
25
|
const begun = await beginWorkerAttempt(input);
|
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { executeDagReconcileRun } from "../../workflows/dag/reconcile-run.js";
|
|
4
4
|
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
|
|
5
5
|
import { appendTaskPoolEvent, findRunByWorkerRunId, getTaskPoolRoot, readTaskPoolState, writeTaskPoolState, } from "./run-store.js";
|
|
6
|
+
import { readRunOwner } from "./run-owner-store.js";
|
|
7
|
+
import { readAttemptLease, releaseAttemptLease } from "./attempt-lease.js";
|
|
6
8
|
const RECOVERABLE_LIVENESS = new Set([
|
|
7
9
|
"active",
|
|
8
10
|
"node-quiet",
|
|
@@ -110,6 +112,17 @@ export async function reconcileTaskPoolAbandon(input) {
|
|
|
110
112
|
idempotent: false,
|
|
111
113
|
evidencePath: operatorRecovery.evidencePath,
|
|
112
114
|
});
|
|
115
|
+
const lease = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
|
|
116
|
+
if (lease?.workerRunId === input.workerRunId) {
|
|
117
|
+
await releaseAttemptLease({
|
|
118
|
+
repoRoot: input.repoRoot,
|
|
119
|
+
featureId: input.featureId,
|
|
120
|
+
taskId: input.taskId,
|
|
121
|
+
token: lease.token,
|
|
122
|
+
workerRunId: input.workerRunId,
|
|
123
|
+
now: input.now,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
113
126
|
return {
|
|
114
127
|
schemaVersion: 1,
|
|
115
128
|
status: "Failed",
|
|
@@ -201,7 +214,6 @@ export async function markTaskPoolFailed(input) {
|
|
|
201
214
|
};
|
|
202
215
|
}
|
|
203
216
|
async function validateWorkerDagAssociation(input) {
|
|
204
|
-
const run = await validateWorkerRunOwnership(input);
|
|
205
217
|
const dagLocated = await locateDagRun(input.repoRoot, input.dagRunId);
|
|
206
218
|
if (!dagLocated) {
|
|
207
219
|
throw new Error(`dag run not found: ${input.dagRunId}`);
|
|
@@ -210,6 +222,39 @@ async function validateWorkerDagAssociation(input) {
|
|
|
210
222
|
if (dagState.runId !== input.dagRunId) {
|
|
211
223
|
throw new Error(`dag run identity mismatch: expected ${input.dagRunId}, found ${dagState.runId}`);
|
|
212
224
|
}
|
|
225
|
+
let run = await findRunByWorkerRunId(input.repoRoot, input.workerRunId);
|
|
226
|
+
if (!run) {
|
|
227
|
+
const owner = await readRunOwner(input.repoRoot, input.workerRunId);
|
|
228
|
+
const association = dagState.workerAssociation;
|
|
229
|
+
const authority = dagState.continuation?.workerContinuationAuthority;
|
|
230
|
+
if (!owner ||
|
|
231
|
+
owner.featureId !== input.featureId ||
|
|
232
|
+
owner.taskId !== input.taskId ||
|
|
233
|
+
owner.workerRunId !== input.workerRunId ||
|
|
234
|
+
owner.revisionId === undefined ||
|
|
235
|
+
input.dagRunId !== input.workerRunId ||
|
|
236
|
+
association?.workerRunId !== input.workerRunId ||
|
|
237
|
+
association.featureId !== input.featureId ||
|
|
238
|
+
association.taskId !== input.taskId ||
|
|
239
|
+
authority?.childWorkerRunId !== input.workerRunId ||
|
|
240
|
+
authority.parentWorkerRunId === input.workerRunId ||
|
|
241
|
+
authority.revisionId !== owner.revisionId) {
|
|
242
|
+
throw new Error(`worker run not found: ${input.workerRunId}`);
|
|
243
|
+
}
|
|
244
|
+
run = {
|
|
245
|
+
schemaVersion: 1,
|
|
246
|
+
batchRunId: owner.batchRunId,
|
|
247
|
+
workerRunId: owner.workerRunId,
|
|
248
|
+
featureId: owner.featureId,
|
|
249
|
+
taskId: owner.taskId,
|
|
250
|
+
status: "run-error",
|
|
251
|
+
attemptKind: "artifact-continuation",
|
|
252
|
+
recordedAt: owner.registeredAt,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
else if (run.featureId !== input.featureId || run.taskId !== input.taskId) {
|
|
256
|
+
throw new Error(`worker run ownership mismatch: expected ${input.featureId}/${input.taskId}, found ${run.featureId}/${run.taskId}`);
|
|
257
|
+
}
|
|
213
258
|
const linked = await proveWorkerDagLink(input.repoRoot, run, input.workerRunId, input.dagRunId);
|
|
214
259
|
if (!linked) {
|
|
215
260
|
throw new Error(`worker run ${input.workerRunId} is not associated with dag run ${input.dagRunId}`);
|
|
@@ -350,6 +350,8 @@ function eventFromRun(run) {
|
|
|
350
350
|
taskId: run.taskId,
|
|
351
351
|
featureId: run.featureId,
|
|
352
352
|
status: run.status,
|
|
353
|
+
...(run.attemptKind ? { attemptKind: run.attemptKind } : {}),
|
|
354
|
+
...(run.continuation ? { continuation: run.continuation } : {}),
|
|
353
355
|
};
|
|
354
356
|
}
|
|
355
357
|
function isNotFound(error) {
|
|
@@ -47,6 +47,8 @@ export function stateFromRun(run) {
|
|
|
47
47
|
status: projectTaskPoolStatusFromRun(run),
|
|
48
48
|
updatedAt: run.recordedAt,
|
|
49
49
|
workerRunId: run.workerRunId,
|
|
50
|
+
...(run.attemptKind ? { attemptKind: run.attemptKind } : {}),
|
|
51
|
+
...(run.continuation ? { continuation: run.continuation } : {}),
|
|
50
52
|
...(run.runRecordPath ? { lastRunRecordPath: run.runRecordPath } : {}),
|
|
51
53
|
...(run.failure ? { failure: run.failure } : {}),
|
|
52
54
|
...(run.failureV2 ? { failureV2: run.failureV2 } : {}),
|