@tea-agent/loop-agent 0.44.0-next.1 → 0.44.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/CHANGELOG.md +379 -416
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
- package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/plan/record-tools.js +861 -0
- package/dist/executors/dag-pi/plan/schema.js +129 -0
- package/dist/executors/dag-pi/plan/session.js +1 -0
- package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +398 -3384
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +112 -19
- package/dist/executors/shell-executor.js +88 -12
- package/dist/executors/shell-presets.js +1 -1
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/infrastructure/console/operation-store.js +48 -14
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +482 -0
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/shared/path-safety.js +2 -2
- package/dist/shared/wizard-local-telemetry.js +51 -0
- package/dist/task/config-types.js +22 -8
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -7
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +62 -10
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/db-connections.js +854 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
- package/dist/worker/console/chat/pi-runtime.js +445 -36
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/repo-browser.js +52 -4
- package/dist/worker/console/chat/routes.js +577 -72
- package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/timeline-message-order.js +22 -0
- package/dist/worker/console/chat/tools.js +34 -0
- package/dist/worker/console/chat/turn-process.js +237 -66
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/dag-confirmation.js +3 -0
- package/dist/worker/console/dag-execution-receipt.js +3 -0
- package/dist/worker/console/operator-actions.js +32 -3
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/prd-intake-bridge.js +5 -12
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
- package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-context.js +2 -0
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/dist/worker/observe/static/console-theme.css +174 -0
- package/dist/worker/observe/static/console-theme.js +36 -0
- package/dist/worker/observe/static/operator-chrome.css +85 -0
- package/dist/worker/observe/static/operator-chrome.js +51 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
- package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +3 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -66
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/dist/workflows/loop/benchmark.js +11 -8
- package/docs/README.md +1 -1
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/docs/templates/backend-test-dag.json +5 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +6 -4
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { completedSourceExecutionSchema } from "./source-execution.js";
|
|
2
|
+
import { currentFrontendSessionIdentity } from "../../workflows/dag/frontend-session-context.js";
|
|
3
|
+
import { sha256OfCanonicalJson } from "../contract/hash.js";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { lstat, mkdir, readdir, rmdir } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { sourceProviderBudgetSchema as ledgerSchema } from "../../application/evaluation/budget.js";
|
|
8
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
9
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
10
|
+
import { getTaskPaths, loadTaskConfig } from "../runtime.js";
|
|
11
|
+
/** Read-only generation snapshot; never create a zero ledger over old receipts. */
|
|
12
|
+
export async function readTaskSourceProviderBudget(input) {
|
|
13
|
+
const root = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts");
|
|
14
|
+
const file = path.join(root, "source-provider-budget.json");
|
|
15
|
+
const exists = await lstat(file).then(() => true).catch(error => { if (error.code === "ENOENT")
|
|
16
|
+
return false; throw error; });
|
|
17
|
+
if (!exists) {
|
|
18
|
+
const files = await readdir(path.join(root, "session-budget")).catch(error => { if (error.code === "ENOENT")
|
|
19
|
+
return []; throw error; });
|
|
20
|
+
if (files.length || input.expectedBudgetId)
|
|
21
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: missing bound source budget");
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const ledger = ledgerSchema.parse(JSON.parse((await readFileBoundarySafe({ boundaryRoot: root, targetPath: file, label: "source provider budget" })).toString("utf8")));
|
|
25
|
+
validateBinding(input, ledger);
|
|
26
|
+
return ledger;
|
|
27
|
+
}
|
|
28
|
+
function validateBinding(input, ledger) {
|
|
29
|
+
if (ledger.taskId !== input.taskId || ledger.maxProviderRequests !== input.maxProviderRequests || input.expectedBudgetId && ledger.budgetId !== input.expectedBudgetId || input.minimumReservedRequests !== undefined && ledger.reservedRequests < input.minimumReservedRequests)
|
|
30
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: source budget binding changed");
|
|
31
|
+
}
|
|
32
|
+
/** The short filesystem critical section spans read, admission and atomic
|
|
33
|
+
* persistence. A stale lock is never stolen on an age heuristic. */
|
|
34
|
+
async function transact(input, reserve) {
|
|
35
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(input.taskId) || !Number.isSafeInteger(input.maxProviderRequests) || input.maxProviderRequests < 1)
|
|
36
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: invalid source budget identity or limit");
|
|
37
|
+
await loadTaskConfig(input.repoRoot, input.taskId); // never recreate a missing task
|
|
38
|
+
const root = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts");
|
|
39
|
+
const rootStat = await lstat(root);
|
|
40
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
41
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: source artifact directory is not a real directory");
|
|
42
|
+
const file = path.join(root, "source-provider-budget.json");
|
|
43
|
+
const lock = path.join(root, ".source-provider-budget.lock");
|
|
44
|
+
let held = false;
|
|
45
|
+
try {
|
|
46
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
await mkdir(lock);
|
|
49
|
+
held = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== "EEXIST")
|
|
54
|
+
throw error;
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!held)
|
|
59
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_BUSY: source budget lock unavailable");
|
|
60
|
+
let ledger;
|
|
61
|
+
try {
|
|
62
|
+
await lstat(file);
|
|
63
|
+
ledger = ledgerSchema.parse(JSON.parse((await readFileBoundarySafe({ boundaryRoot: root, targetPath: file, label: "source provider budget" })).toString("utf8")));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code !== "ENOENT")
|
|
67
|
+
throw Error(`FRONTEND_PROVIDER_BUDGET_INVALID: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
if (input.expectedBudgetId)
|
|
69
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: bound task budget is missing");
|
|
70
|
+
// Existing attempts cannot be silently imported as zero-cost history.
|
|
71
|
+
const previous = await readdir(path.join(root, "session-budget")).catch(error => {
|
|
72
|
+
if (error.code === "ENOENT")
|
|
73
|
+
return [];
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
if (previous.length)
|
|
77
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: source receipts have no bound budget");
|
|
78
|
+
ledger = { schemaVersion: 1, taskId: input.taskId, budgetId: randomUUID(), maxProviderRequests: input.maxProviderRequests, reservedRequests: 0 };
|
|
79
|
+
}
|
|
80
|
+
validateBinding(input, ledger);
|
|
81
|
+
if (reserve) {
|
|
82
|
+
if (ledger.reservedRequests >= ledger.maxProviderRequests)
|
|
83
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_EXHAUSTED: source request quota reached");
|
|
84
|
+
const identity = currentFrontendSessionIdentity();
|
|
85
|
+
if (identity) {
|
|
86
|
+
if (identity.executionKind !== "model" || identity.taskId && identity.taskId !== input.taskId)
|
|
87
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: request belongs to another session/task");
|
|
88
|
+
const key = `${identity.runId ?? "task"}:${identity.sessionId}`;
|
|
89
|
+
const previous = ledger.requestSessions?.[key];
|
|
90
|
+
if (previous && sha256OfCanonicalJson(previous.identity) !== sha256OfCanonicalJson(identity))
|
|
91
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: session identity changed");
|
|
92
|
+
ledger.requestSessions ??= {};
|
|
93
|
+
ledger.requestSessions[key] = { identity, reservedRequests: (previous?.reservedRequests ?? 0) + 1 };
|
|
94
|
+
}
|
|
95
|
+
ledger.reservedRequests++;
|
|
96
|
+
}
|
|
97
|
+
// A failed write grants no admission. A successful reservation is never refunded.
|
|
98
|
+
await writeJsonAtomic(file, ledger, { repoRoot: input.repoRoot });
|
|
99
|
+
return ledger;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (held)
|
|
103
|
+
await rmdir(lock);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function reserveTaskSourceProviderRequest(input) {
|
|
107
|
+
try {
|
|
108
|
+
await transact(input, true);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
throw Error(message.startsWith("FRONTEND_PROVIDER_") ? message : `FRONTEND_PROVIDER_BUDGET_PERSIST_FAILED: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Initialize before the adapter writes its first incomplete session receipt. */
|
|
116
|
+
export async function openTaskSourceProviderBudget(input) {
|
|
117
|
+
await transact(input, false);
|
|
118
|
+
return () => reserveTaskSourceProviderRequest(input);
|
|
119
|
+
}
|
|
120
|
+
/** Check the closed set of source sessions registered by durable admission.
|
|
121
|
+
* Receipt copies deduplicate only when their contents are identical. */
|
|
122
|
+
export function summarizeBoundSourceUsage(ledger, receipts) {
|
|
123
|
+
ledgerSchema.parse(ledger);
|
|
124
|
+
const diagnostics = [];
|
|
125
|
+
const sessions = Object.values(ledger.requestSessions ?? {});
|
|
126
|
+
const sources = sessions.filter(session => session.identity.runId === null);
|
|
127
|
+
const unattributedRequests = ledger.reservedRequests - sessions.reduce((sum, session) => sum + session.reservedRequests, 0);
|
|
128
|
+
if (unattributedRequests)
|
|
129
|
+
diagnostics.push("unattributed-task-requests");
|
|
130
|
+
const byId = new Map();
|
|
131
|
+
for (const receipt of receipts) {
|
|
132
|
+
if (receipt.identity.runId !== null)
|
|
133
|
+
continue;
|
|
134
|
+
const key = receipt.identity.sessionId;
|
|
135
|
+
const previous = byId.get(key);
|
|
136
|
+
if (previous && sha256OfCanonicalJson(previous) !== sha256OfCanonicalJson(receipt))
|
|
137
|
+
diagnostics.push(`conflicting-source-receipt:${key}`);
|
|
138
|
+
byId.set(key, receipt);
|
|
139
|
+
if (!sources.some(session => session.identity.sessionId === key))
|
|
140
|
+
diagnostics.push(`unbound-source-receipt:${key}`);
|
|
141
|
+
}
|
|
142
|
+
let totalTokens = 0;
|
|
143
|
+
for (const session of sources) {
|
|
144
|
+
const receipt = byId.get(session.identity.sessionId);
|
|
145
|
+
if (!receipt || session.identity.taskId !== ledger.taskId || !session.identity.sourceDigest || sha256OfCanonicalJson(session.identity) !== sha256OfCanonicalJson(receipt.identity)) {
|
|
146
|
+
diagnostics.push(`source-session-binding:${session.identity.sessionId}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (receipt.completion !== "complete" || !receipt.terminal || receipt.actual.requestCount !== session.reservedRequests)
|
|
150
|
+
diagnostics.push(`source-session-incomplete:${session.identity.sessionId}`);
|
|
151
|
+
const tokens = receipt.actual.totalTokens;
|
|
152
|
+
if (tokens === null || !Number.isFinite(tokens) || tokens < 0)
|
|
153
|
+
diagnostics.push(`source-tokens-unknown:${session.identity.sessionId}`);
|
|
154
|
+
else
|
|
155
|
+
totalTokens += tokens;
|
|
156
|
+
if (receipt.attempts.length && (receipt.attempts.some(attempt => attempt.completion !== "complete" || attempt.actual?.requestCount == null || attempt.actual?.totalTokens == null) || receipt.attempts.reduce((sum, attempt) => sum + (attempt.actual?.requestCount ?? 0), 0) !== session.reservedRequests || receipt.attempts.reduce((sum, attempt) => sum + (attempt.actual?.totalTokens ?? 0), 0) !== tokens))
|
|
157
|
+
diagnostics.push(`source-attempt-usage-mismatch:${session.identity.sessionId}`);
|
|
158
|
+
}
|
|
159
|
+
return { complete: diagnostics.length === 0, diagnostics, sourceSessions: sources.length, sourceRequests: sources.reduce((sum, session) => sum + session.reservedRequests, 0), unattributedRequests, totalTokens: diagnostics.length ? null : totalTokens };
|
|
160
|
+
}
|
|
161
|
+
const receiptHash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
162
|
+
/** Freeze actual bytes, not a caller-supplied token total. Concurrent changes
|
|
163
|
+
* remain diagnostic evidence rather than a cheaper snapshot. */
|
|
164
|
+
export async function freezeTaskSourceUsage(input, ledger) {
|
|
165
|
+
const root = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts");
|
|
166
|
+
const evidence = { receipts: [], executions: [], diagnostics: [] };
|
|
167
|
+
try {
|
|
168
|
+
const files = await readdir(path.join(root, "session-budget")).catch(error => { if (error.code === "ENOENT")
|
|
169
|
+
return []; throw error; });
|
|
170
|
+
for (const name of files.sort()) {
|
|
171
|
+
const relative = `session-budget/${name}`;
|
|
172
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, relative), label: "source usage receipt" });
|
|
173
|
+
evidence.receipts.push({ path: relative, sha256: receiptHash(bytes), content: bytes.toString("utf8") });
|
|
174
|
+
}
|
|
175
|
+
const executionIds = new Set(Object.values(ledger.requestSessions ?? {}).filter(row => row.identity.runId === null).map(row => row.identity.sourceExecutionId).filter((id) => !!id));
|
|
176
|
+
for (const id of [...executionIds].sort()) {
|
|
177
|
+
const relative = `source-executions/${id}.json`;
|
|
178
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, relative), label: "source execution" });
|
|
179
|
+
evidence.executions.push({ path: relative, sha256: receiptHash(bytes), content: bytes.toString("utf8") });
|
|
180
|
+
}
|
|
181
|
+
for (const ref of [...evidence.receipts, ...evidence.executions]) {
|
|
182
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, ref.path), label: "source usage receipt" });
|
|
183
|
+
if (receiptHash(bytes) !== ref.sha256)
|
|
184
|
+
throw Error("source-receipt-changed-during-freeze");
|
|
185
|
+
}
|
|
186
|
+
const after = await readTaskSourceProviderBudget(input);
|
|
187
|
+
if (sha256OfCanonicalJson(after ?? null) !== sha256OfCanonicalJson(ledger))
|
|
188
|
+
throw Error("source-ledger-changed-during-freeze");
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
evidence.diagnostics.push(error instanceof Error ? error.message : String(error));
|
|
192
|
+
}
|
|
193
|
+
return evidence;
|
|
194
|
+
}
|
|
195
|
+
export function summarizeFrozenSourceUsage(binding) {
|
|
196
|
+
const diagnostics = [...(binding.evidence?.diagnostics ?? [])];
|
|
197
|
+
const receipts = [];
|
|
198
|
+
if (!binding.evidence)
|
|
199
|
+
diagnostics.push("missing-frozen-source-evidence");
|
|
200
|
+
for (const ref of binding.evidence?.receipts ?? []) {
|
|
201
|
+
try {
|
|
202
|
+
if (!/^session-budget\/[^/\\]+\.json$/.test(ref.path) || receiptHash(ref.content) !== ref.sha256)
|
|
203
|
+
throw Error("source-receipt-hash-or-path");
|
|
204
|
+
const receipt = JSON.parse(ref.content);
|
|
205
|
+
if (receipt.schemaVersion !== 2 || receipt.policyVersion !== "frontend-session-observation-v2" || !receipt.identity || receipt.identity.runId !== null || !receipt.actual || !Array.isArray(receipt.attempts) || !Number.isFinite(Date.parse(receipt.startedAt)) || !receipt.finishedAt || Date.parse(receipt.finishedAt) < Date.parse(receipt.startedAt) || !Number.isFinite(Date.parse(receipt.finishedAt)))
|
|
206
|
+
throw Error("invalid-source-receipt");
|
|
207
|
+
receipts.push(receipt);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
diagnostics.push(`invalid-frozen-source-receipt:${ref.path}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const executions = new Map();
|
|
214
|
+
const required = new Set(Object.values(binding.ledger.requestSessions ?? {}).filter(row => row.identity.runId === null).map(row => row.identity.sourceExecutionId).filter((id) => !!id));
|
|
215
|
+
for (const ref of binding.evidence?.executions ?? []) {
|
|
216
|
+
try {
|
|
217
|
+
if (receiptHash(ref.content) !== ref.sha256)
|
|
218
|
+
throw Error("execution hash");
|
|
219
|
+
const execution = completedSourceExecutionSchema.parse(JSON.parse(ref.content));
|
|
220
|
+
if (ref.path !== `source-executions/${execution.executionId}.json` || execution.taskId !== binding.ledger.taskId || !required.has(execution.executionId) || executions.has(execution.executionId))
|
|
221
|
+
throw Error("execution identity");
|
|
222
|
+
executions.set(execution.executionId, execution);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
diagnostics.push(`invalid-source-execution:${ref.path}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const id of required)
|
|
229
|
+
if (!executions.has(id))
|
|
230
|
+
diagnostics.push(`missing-source-execution:${id}`);
|
|
231
|
+
for (const receipt of receipts) {
|
|
232
|
+
const id = receipt.identity.sourceExecutionId;
|
|
233
|
+
if (!id) {
|
|
234
|
+
diagnostics.push(`unregistered-source-execution:${receipt.identity.sessionId}`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const execution = executions.get(id);
|
|
238
|
+
if (!execution || Date.parse(receipt.startedAt) < Date.parse(execution.startedAt) || Date.parse(receipt.finishedAt) > Date.parse(execution.finishedAt) || (execution.phase === "semantic-intake") !== (receipt.identity.phase === "semantic-intake"))
|
|
239
|
+
diagnostics.push(`source-execution-session-mismatch:${receipt.identity.sessionId}`);
|
|
240
|
+
}
|
|
241
|
+
const relation = (snapshot) => {
|
|
242
|
+
if (!binding.contract)
|
|
243
|
+
return "unbound";
|
|
244
|
+
if (snapshot.revision === null && snapshot.canonicalHash === null && snapshot.observedCanonicalHash === null)
|
|
245
|
+
return "pre-contract";
|
|
246
|
+
if (snapshot.revision === null || snapshot.canonicalHash === null || snapshot.observedCanonicalHash !== snapshot.canonicalHash)
|
|
247
|
+
return "conflict";
|
|
248
|
+
if (snapshot.revision > binding.contract.revision)
|
|
249
|
+
return "future-revision";
|
|
250
|
+
if (snapshot.revision < binding.contract.revision)
|
|
251
|
+
return "prior-revision";
|
|
252
|
+
return snapshot.canonicalHash === binding.contract.canonicalHash ? "current" : "conflict";
|
|
253
|
+
};
|
|
254
|
+
const executionContracts = [...executions.values()].map(execution => ({ executionId: execution.executionId, before: relation(execution.beforeContract), after: relation(execution.afterContract) }));
|
|
255
|
+
for (const row of executionContracts)
|
|
256
|
+
if ([row.before, row.after].some(value => ["unbound", "conflict", "future-revision"].includes(value)))
|
|
257
|
+
diagnostics.push(`source-execution-contract:${row.executionId}`);
|
|
258
|
+
try {
|
|
259
|
+
const usage = summarizeBoundSourceUsage(binding.ledger, receipts);
|
|
260
|
+
diagnostics.push(...usage.diagnostics);
|
|
261
|
+
// Non-source requests in a task-lifetime ledger have not been priced by
|
|
262
|
+
// this source-only proof. Keep their existing unknown-cost marker.
|
|
263
|
+
if (binding.ledger.reservedRequests !== usage.sourceRequests)
|
|
264
|
+
diagnostics.push("non-source-task-cost-unproven");
|
|
265
|
+
return { ...usage, executionContracts, complete: diagnostics.length === 0, diagnostics, totalTokens: diagnostics.length ? null : usage.totalTokens };
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { executionContracts, complete: false, diagnostics: [...diagnostics, "invalid-source-usage"], sourceSessions: null, sourceRequests: null, unattributedRequests: null, totalTokens: null };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Evaluation checks the original files against the frozen generation bytes.
|
|
272
|
+
* It never reads today's task ledger as yesterday's cost. */
|
|
273
|
+
export async function verifyFrozenSourceUsage(repoRoot, binding) {
|
|
274
|
+
const summary = summarizeFrozenSourceUsage(binding);
|
|
275
|
+
const diagnostics = [...summary.diagnostics];
|
|
276
|
+
const root = path.join(getTaskPaths(repoRoot, binding.ledger.taskId).taskDir, "artifacts");
|
|
277
|
+
for (const ref of [...(binding.evidence?.receipts ?? []), ...(binding.evidence?.executions ?? [])]) {
|
|
278
|
+
try {
|
|
279
|
+
if (!/^(?:session-budget|source-executions)\/[^/\\]+\.json$/.test(ref.path))
|
|
280
|
+
throw Error("invalid path");
|
|
281
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.join(root, ref.path), label: "frozen source usage" });
|
|
282
|
+
if (receiptHash(bytes) !== ref.sha256)
|
|
283
|
+
throw Error("changed receipt");
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
diagnostics.push(`source-evidence-missing-or-changed:${ref.path}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { ...summary, complete: diagnostics.length === 0, diagnostics, totalTokens: diagnostics.length ? null : summary.totalTokens };
|
|
290
|
+
}
|
|
@@ -102,43 +102,14 @@ export function splitFinalAssistantBlocks(blocks, options = {}) {
|
|
|
102
102
|
// after text that is already visible. Keep the whole live message in the
|
|
103
103
|
// chronological process subtree so that update appends the tool in place
|
|
104
104
|
// instead of reparenting the earlier text from answer -> process.
|
|
105
|
-
if (options.isStreaming
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
let lastProcessIndex = -1;
|
|
109
|
-
for (let i = displayable.length - 1; i >= 0; i--) {
|
|
110
|
-
if (!isFinalAnswerBlock(displayable[i])) {
|
|
111
|
-
lastProcessIndex = i;
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (lastProcessIndex === -1) {
|
|
116
|
-
return { answerBlocks: displayable, processBlocks: [] };
|
|
117
|
-
}
|
|
118
|
-
const trailing = displayable.slice(lastProcessIndex + 1);
|
|
119
|
-
if (trailing.length > 0) {
|
|
120
|
-
return {
|
|
121
|
-
answerBlocks: trailing,
|
|
122
|
-
processBlocks: displayable.slice(0, lastProcessIndex + 1),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
// The stage ends on a non-text block (a tool call after the prose, or a final
|
|
126
|
-
// thinking block). Returning an empty answer here hid the whole reply inside
|
|
127
|
-
// the collapsed process subtree; keep the last prose block as the visible
|
|
128
|
-
// answer instead (2026-09-08).
|
|
129
|
-
let lastTextIndex = -1;
|
|
130
|
-
for (let i = displayable.length - 1; i >= 0; i--) {
|
|
131
|
-
if (isFinalAnswerBlock(displayable[i])) {
|
|
132
|
-
lastTextIndex = i;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (lastTextIndex === -1) {
|
|
105
|
+
if (options.isStreaming ||
|
|
106
|
+
displayable.some((block) => block.type === "toolCall")) {
|
|
137
107
|
return { answerBlocks: [], processBlocks: displayable };
|
|
138
108
|
}
|
|
109
|
+
const answerBlocks = displayable.filter((block) => isFinalAnswerBlock(block) && block.text.trim() !== "");
|
|
139
110
|
return {
|
|
140
|
-
answerBlocks
|
|
141
|
-
processBlocks: displayable.filter((
|
|
111
|
+
answerBlocks,
|
|
112
|
+
processBlocks: displayable.filter((block) => !isFinalAnswerBlock(block)),
|
|
142
113
|
};
|
|
143
114
|
}
|
|
144
115
|
export function joinAssistantText(blocks) {
|
|
@@ -327,18 +327,62 @@ export function projectChatToolEventPayload(input) {
|
|
|
327
327
|
toolName: input.toolName,
|
|
328
328
|
argsPreview: preview,
|
|
329
329
|
previewHash,
|
|
330
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
331
|
+
...(input.parentToolCallId
|
|
332
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
333
|
+
: {}),
|
|
330
334
|
};
|
|
331
335
|
}
|
|
332
336
|
const { preview, previewHash } = projectToolResultPreview(input.value);
|
|
337
|
+
if (input.kind === "tool_update") {
|
|
338
|
+
return {
|
|
339
|
+
toolCallId: input.toolCallId,
|
|
340
|
+
toolName: input.toolName,
|
|
341
|
+
resultPreview: preview,
|
|
342
|
+
previewHash,
|
|
343
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
344
|
+
...(input.parentToolCallId
|
|
345
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
346
|
+
: {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
333
349
|
return {
|
|
334
350
|
toolCallId: input.toolCallId,
|
|
335
351
|
toolName: input.toolName,
|
|
336
352
|
resultPreview: preview,
|
|
337
353
|
previewHash,
|
|
338
354
|
isError: Boolean(input.isError),
|
|
355
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
356
|
+
...(input.parentToolCallId
|
|
357
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
358
|
+
: {}),
|
|
339
359
|
...(input.operationId ? { operationId: input.operationId } : {}),
|
|
340
360
|
};
|
|
341
361
|
}
|
|
362
|
+
export function projectUnknownChatEventPayload(input) {
|
|
363
|
+
const redact = (value, depth) => {
|
|
364
|
+
if (depth >= 6)
|
|
365
|
+
return "[DEPTH_LIMIT]";
|
|
366
|
+
if (Array.isArray(value)) {
|
|
367
|
+
return value.slice(0, 50).map((item) => redact(item, depth + 1));
|
|
368
|
+
}
|
|
369
|
+
if (!value || typeof value !== "object")
|
|
370
|
+
return value;
|
|
371
|
+
const result = {};
|
|
372
|
+
for (const [key, child] of Object.entries(value).slice(0, 100)) {
|
|
373
|
+
result[key] = /authorization|cookie|password|secret|token|api[-_]?key/i.test(key)
|
|
374
|
+
? "[REDACTED]"
|
|
375
|
+
: redact(child, depth + 1);
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
};
|
|
379
|
+
const { preview, previewHash } = boundedRedactedJson(redact(input.value, 0));
|
|
380
|
+
return {
|
|
381
|
+
eventType: input.eventType.slice(0, 120),
|
|
382
|
+
preview,
|
|
383
|
+
previewHash,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
342
386
|
export function projectOperationEventSummary(event) {
|
|
343
387
|
const messagePreview = boundedRedactedText(event.message, 600);
|
|
344
388
|
const dataPreview = event.data === undefined
|
|
@@ -406,22 +450,27 @@ function dropOneByPriority(ring) {
|
|
|
406
450
|
return events.shift();
|
|
407
451
|
}
|
|
408
452
|
/**
|
|
409
|
-
* D2 coalescing decision: merge
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* (their presence makes the tail check fail), so an
|
|
413
|
-
* update → tool_call → update sequence never merges across the tool boundary.
|
|
453
|
+
* D2 coalescing decision: merge high-frequency cumulative text/tool updates
|
|
454
|
+
* into a matching ring tail inside the coalescing window. Structural events
|
|
455
|
+
* close the window implicitly (their presence makes the tail check fail).
|
|
414
456
|
* `partial.at` (when present) drives the clock so tests can inject window
|
|
415
457
|
* boundaries deterministically without fake timers.
|
|
416
458
|
*/
|
|
417
459
|
function shouldCoalesce(ring, turnId, partial) {
|
|
418
|
-
if (partial.kind !== "message_update")
|
|
419
|
-
return false;
|
|
420
460
|
const tail = ring.events.at(-1);
|
|
421
|
-
if (!tail || tail.kind !==
|
|
461
|
+
if (!tail || tail.kind !== partial.kind)
|
|
422
462
|
return false;
|
|
423
463
|
if (tail.turnId !== turnId)
|
|
424
464
|
return false;
|
|
465
|
+
if (partial.kind === "tool_update") {
|
|
466
|
+
const currentId = partial.data.toolCallId;
|
|
467
|
+
const tailId = tail.data.toolCallId;
|
|
468
|
+
if (!currentId || currentId !== tailId)
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
else if (partial.kind !== "message_update") {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
425
474
|
const nowMs = partial.at ? Date.parse(partial.at) : Date.now();
|
|
426
475
|
if (Number.isNaN(nowMs))
|
|
427
476
|
return false;
|
|
@@ -1016,12 +1065,12 @@ export function createChatEventStore(options) {
|
|
|
1016
1065
|
ring.approxBytes += byteHint(live);
|
|
1017
1066
|
}
|
|
1018
1067
|
enforceBounds(sessionId, ring);
|
|
1019
|
-
// B5-1:
|
|
1068
|
+
// B5-1: high-frequency cumulative updates defer the persist;
|
|
1020
1069
|
// every other kind (message_end / tool_call / error / turn signals …)
|
|
1021
1070
|
// still persists synchronously. Because persistNow writes the FULL
|
|
1022
1071
|
// in-memory ring, one flush also lands any earlier pending updates in
|
|
1023
1072
|
// order — no two-phase flush is needed.
|
|
1024
|
-
if (partial.kind === "message_update") {
|
|
1073
|
+
if (partial.kind === "message_update" || partial.kind === "tool_update") {
|
|
1025
1074
|
const atMs = partial.at ? Date.parse(partial.at) : Date.now();
|
|
1026
1075
|
if (!Number.isNaN(atMs)) {
|
|
1027
1076
|
persistDebounced(sessionId, ring, atMs);
|
|
@@ -1074,6 +1123,9 @@ export function createChatEventStore(options) {
|
|
|
1074
1123
|
epoch(sessionId) {
|
|
1075
1124
|
return ringFor(sessionId).epoch;
|
|
1076
1125
|
},
|
|
1126
|
+
latestSeq(sessionId) {
|
|
1127
|
+
return ringFor(sessionId).nextSeq - 1;
|
|
1128
|
+
},
|
|
1077
1129
|
snapshot(sessionId) {
|
|
1078
1130
|
const ring = rings.get(sessionId) ?? loadRing(sessionId);
|
|
1079
1131
|
if (ring)
|