@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -267
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +0 -5
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +231 -3784
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/task/config-types.js +7 -34
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +235 -13
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +86 -44
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +133 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +139 -60
- package/dist/worker/console/index.js +0 -3
- package/dist/worker/console/operation-store.js +6 -226
- package/dist/worker/console/operator-actions.js +7 -169
- package/dist/worker/console/pi-plugins.js +3 -1
- package/dist/worker/console/server.js +19 -4
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DQ5jlYVU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BeTVCzH9.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-DassREBi.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-DyeNX8Ek.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-BXb_zxGx.js} +1 -1
- package/dist/worker/console/static/assets/channel-B_lofstT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-B9F9gxGC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-Byd5zHEQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-BqoxMJk9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-CT7d6YZw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CdiNyN0N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-BGw9bpVc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-DiDVlVB1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-BhHf-4yK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-B79pEgwv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-Cb70EcUV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-ieUI0-eY.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-ieUI0-eY.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-DBHLnAf1.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-BT_JRsOH.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-Bss-msa3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-B8Gr24pt.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-BrjU70Jw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cp_JpPGd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-BWKICEP-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D3z_UNR_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-_Ywm390x.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-D9QYQQMB.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BNgXNKAr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-QRLnkPCg.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-B9fOduNi.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-FSEdtPN2.js} +1 -1
- package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
- package/dist/worker/console/static/assets/index-DtPrwu7M.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-D5Bjg9dm.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BlNRkWvf.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-BdJiw3SL.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-CsDNlYht.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-8g9dbEmX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-ChyQH_ut.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-DwYoCTYt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-T_b2rS17.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-1Cr7WMhH.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-Bq4qM59o.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-ytFgR1RL.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CnUdUVYX.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-_5rJqG6R.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-D5imyGbt.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-DvgDn3wL.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BViu7duc.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-kNxnG4_w.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BcULKg2P.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DyTzlcWH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-DmPveIjt.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-9r-iFy0R.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-D3fIXdvM.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-BCSS9SBg.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/console/workspace-context.js +1 -111
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/views/dag-inspector.js +2 -7
- package/dist/worker/observe/static/views/session-timeline.js +46 -15
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/init-hybrid.js +494 -685
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +4 -732
- package/dist/workflows/dag/prompt.js +1 -34
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +2 -10
- package/dist/workflows/dag/rerun-run.js +0 -23
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +104 -214
- package/dist/workflows/dag/runner.js +122 -429
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/types.js +12 -239
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +0 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* `createDefaultPiSdkBindings()`.
|
|
27
27
|
*/
|
|
28
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
29
|
-
import { access } from "node:fs/promises";
|
|
29
|
+
import { access, mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
|
|
32
32
|
import { parseProviderError, } from "./provider-error.js";
|
|
@@ -67,10 +67,11 @@ export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
|
67
67
|
"You are General Operator Chat.",
|
|
68
68
|
"Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.",
|
|
69
69
|
"No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
|
|
70
|
-
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait
|
|
71
|
-
"
|
|
70
|
+
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
|
|
71
|
+
"Failure: terminal runSummary first. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume; do not re-advance a run-failed task. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
|
|
72
72
|
"Iterate toward the goal in bounded rounds: first summarize the previous round's error in one line, fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
|
|
73
73
|
"Continue until success, user stop, or no safe recovery remains; then stop and report failure/evidence. Never create a new task for a transient failure or replace repair-pi with direct edits.",
|
|
74
|
+
"Narrate long-DAG progress briefly at start, meaningful phase changes/findings, verification failure/recovery, prolonged quiet and terminal state: say what finished and what is next. Never narrate unchanged operationWait polls or heartbeats, or claim beyond structured facts.",
|
|
74
75
|
].join("\n");
|
|
75
76
|
/** Compose the inspectable system prompt actually injected into Operator Chat. */
|
|
76
77
|
export function composeOperatorChatSystemPrompt(input) {
|
|
@@ -930,6 +931,44 @@ export class PiSessionInitFailedError extends Error {
|
|
|
930
931
|
this.name = "PiSessionInitFailedError";
|
|
931
932
|
}
|
|
932
933
|
}
|
|
934
|
+
async function readModelConfigFileVersion(filePath) {
|
|
935
|
+
try {
|
|
936
|
+
const value = await stat(filePath, { bigint: true });
|
|
937
|
+
return {
|
|
938
|
+
exists: true,
|
|
939
|
+
dev: value.dev,
|
|
940
|
+
ino: value.ino,
|
|
941
|
+
size: value.size,
|
|
942
|
+
mtimeNs: value.mtimeNs,
|
|
943
|
+
ctimeNs: value.ctimeNs,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
if (error !== null &&
|
|
948
|
+
typeof error === "object" &&
|
|
949
|
+
"code" in error &&
|
|
950
|
+
error.code === "ENOENT") {
|
|
951
|
+
return { exists: false };
|
|
952
|
+
}
|
|
953
|
+
throw new Error("MODEL_CONFIG_CHECK_FAILED");
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function sameModelConfigFileVersion(left, right) {
|
|
957
|
+
if (!left.exists || !right.exists)
|
|
958
|
+
return left.exists === right.exists;
|
|
959
|
+
return (left.dev === right.dev &&
|
|
960
|
+
left.ino === right.ino &&
|
|
961
|
+
left.size === right.size &&
|
|
962
|
+
left.mtimeNs === right.mtimeNs &&
|
|
963
|
+
left.ctimeNs === right.ctimeNs);
|
|
964
|
+
}
|
|
965
|
+
async function captureModelConfigFile(agentDir) {
|
|
966
|
+
const filePath = path.join(agentDir, "models.json");
|
|
967
|
+
return {
|
|
968
|
+
path: filePath,
|
|
969
|
+
version: await readModelConfigFileVersion(filePath),
|
|
970
|
+
};
|
|
971
|
+
}
|
|
933
972
|
/**
|
|
934
973
|
* Console Pi runtime — holds Chat sessions and drives prompt() with three-gate
|
|
935
974
|
* tool enforcement. Session-per-sessionId; cross-request reuse via open().
|
|
@@ -939,6 +978,8 @@ export class ConsolePiRuntime {
|
|
|
939
978
|
loader;
|
|
940
979
|
sessions = new Map();
|
|
941
980
|
modelRuntimes = new Map();
|
|
981
|
+
/** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
|
|
982
|
+
modelConfigFiles = new Map();
|
|
942
983
|
sessionManagers = new Map();
|
|
943
984
|
/** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
|
|
944
985
|
serviceScopes = new Map();
|
|
@@ -948,6 +989,8 @@ export class ConsolePiRuntime {
|
|
|
948
989
|
reloadInFlight = new Set();
|
|
949
990
|
/** Synchronous prompt ownership per session (admission before first await). */
|
|
950
991
|
promptOwners = new Set();
|
|
992
|
+
/** Steering requests accepted by the SDK but not yet consumed into Pi context. */
|
|
993
|
+
pendingSteering = new Map();
|
|
951
994
|
/** At most one detached automatic-title task per durable Console session. */
|
|
952
995
|
titleInFlight = new Set();
|
|
953
996
|
/** Composer thinking selection ("auto" = no explicit override). */
|
|
@@ -1262,6 +1305,15 @@ export class ConsolePiRuntime {
|
|
|
1262
1305
|
if (!sessionFile) {
|
|
1263
1306
|
throw new Error(`chat session shell missing sessionFile for ${sessionId}`);
|
|
1264
1307
|
}
|
|
1308
|
+
// Pi's SessionManager.create deliberately postpones writing a JSONL until
|
|
1309
|
+
// an assistant response. Materialize a valid header now, otherwise Console
|
|
1310
|
+
// could persist a path that disappears on a restart before first reply.
|
|
1311
|
+
await this.bindings.materializeSessionShell?.({
|
|
1312
|
+
sessionManager,
|
|
1313
|
+
sessionId,
|
|
1314
|
+
sessionFile,
|
|
1315
|
+
cwd: this.options.cwd,
|
|
1316
|
+
});
|
|
1265
1317
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
1266
1318
|
this.thinkingSelections.set(sessionId, "auto");
|
|
1267
1319
|
// Detached single-flight materialization; dependent routes await the same promise.
|
|
@@ -1340,6 +1392,9 @@ export class ConsolePiRuntime {
|
|
|
1340
1392
|
if (this.disposedSessions.has(sessionId))
|
|
1341
1393
|
return;
|
|
1342
1394
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
1395
|
+
// Capture before services parse models.json. A concurrent edit can therefore
|
|
1396
|
+
// cause at most one harmless extra refresh, never a missed next-turn refresh.
|
|
1397
|
+
const modelConfigFile = await captureModelConfigFile(agentDir);
|
|
1343
1398
|
const appendSystemPrompt = [
|
|
1344
1399
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
1345
1400
|
...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
@@ -1399,6 +1454,7 @@ export class ConsolePiRuntime {
|
|
|
1399
1454
|
this.sessions.set(sessionId, session);
|
|
1400
1455
|
this.sessionManagers.set(sessionId, init.sessionManager);
|
|
1401
1456
|
this.modelRuntimes.set(sessionId, modelRuntime);
|
|
1457
|
+
this.modelConfigFiles.set(sessionId, modelConfigFile);
|
|
1402
1458
|
this.sessionModels.set(sessionId, model);
|
|
1403
1459
|
this.serviceScopes.set(sessionId, {
|
|
1404
1460
|
resourceLoader: services.resourceLoader,
|
|
@@ -1419,6 +1475,7 @@ export class ConsolePiRuntime {
|
|
|
1419
1475
|
this.sessions.delete(sessionId);
|
|
1420
1476
|
this.sessionManagers.delete(sessionId);
|
|
1421
1477
|
this.modelRuntimes.delete(sessionId);
|
|
1478
|
+
this.modelConfigFiles.delete(sessionId);
|
|
1422
1479
|
this.sessionModels.delete(sessionId);
|
|
1423
1480
|
this.serviceScopes.delete(sessionId);
|
|
1424
1481
|
this.revisions.delete(sessionId);
|
|
@@ -1497,6 +1554,7 @@ export class ConsolePiRuntime {
|
|
|
1497
1554
|
this.sessions.set(sessionId, built.session);
|
|
1498
1555
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
1499
1556
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
1557
|
+
this.modelConfigFiles.set(sessionId, built.modelConfigFile);
|
|
1500
1558
|
this.sessionModels.set(sessionId, init.model);
|
|
1501
1559
|
this.serviceScopes.set(sessionId, built.services);
|
|
1502
1560
|
this.revisions.set(sessionId, 1);
|
|
@@ -1568,11 +1626,69 @@ export class ConsolePiRuntime {
|
|
|
1568
1626
|
if (!resolved)
|
|
1569
1627
|
throw new Error("MODEL_NOT_AVAILABLE");
|
|
1570
1628
|
await session.setModel(resolved);
|
|
1571
|
-
|
|
1572
|
-
return {
|
|
1629
|
+
const applied = {
|
|
1573
1630
|
provider: session.model?.provider ?? model.provider,
|
|
1574
1631
|
modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
|
|
1575
1632
|
};
|
|
1633
|
+
this.sessionModels.set(sessionId, applied);
|
|
1634
|
+
this.bumpRevision(sessionId);
|
|
1635
|
+
return applied;
|
|
1636
|
+
}
|
|
1637
|
+
async refreshModelConfiguration(sessionId, mode) {
|
|
1638
|
+
const session = this.sessions.get(sessionId);
|
|
1639
|
+
const modelRuntime = this.modelRuntimes.get(sessionId);
|
|
1640
|
+
const configFile = this.modelConfigFiles.get(sessionId);
|
|
1641
|
+
if (!session || !modelRuntime || !configFile) {
|
|
1642
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
1643
|
+
}
|
|
1644
|
+
const nextVersion = await readModelConfigFileVersion(configFile.path);
|
|
1645
|
+
if (mode === "if-changed" &&
|
|
1646
|
+
sameModelConfigFileVersion(configFile.version, nextVersion)) {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
if (typeof modelRuntime.refresh !== "function") {
|
|
1650
|
+
throw new Error("MODEL_CONFIG_REFRESH_UNSUPPORTED");
|
|
1651
|
+
}
|
|
1652
|
+
const stored = this.sessionModels.get(sessionId);
|
|
1653
|
+
const provider = session.model?.provider ?? stored?.provider;
|
|
1654
|
+
const modelId = session.model?.id ?? session.model?.modelId ?? stored?.modelId;
|
|
1655
|
+
try {
|
|
1656
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
1657
|
+
if (typeof modelRuntime.getError === "function" && modelRuntime.getError()) {
|
|
1658
|
+
throw new Error("MODEL_CONFIG_REFRESH_FAILED");
|
|
1659
|
+
}
|
|
1660
|
+
if (provider && modelId) {
|
|
1661
|
+
if (typeof session.setModel !== "function") {
|
|
1662
|
+
throw new Error("MODEL_CONFIG_REBIND_UNSUPPORTED");
|
|
1663
|
+
}
|
|
1664
|
+
const resolved = this.bindings.resolveModel({
|
|
1665
|
+
modelRuntime,
|
|
1666
|
+
provider,
|
|
1667
|
+
modelId,
|
|
1668
|
+
});
|
|
1669
|
+
if (!resolved) {
|
|
1670
|
+
throw new Error("MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH");
|
|
1671
|
+
}
|
|
1672
|
+
await session.setModel(resolved);
|
|
1673
|
+
this.sessionModels.set(sessionId, { provider, modelId });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
catch (error) {
|
|
1677
|
+
if (error instanceof Error &&
|
|
1678
|
+
(error.message === "MODEL_CONFIG_REBIND_UNSUPPORTED" ||
|
|
1679
|
+
error.message === "MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH")) {
|
|
1680
|
+
throw error;
|
|
1681
|
+
}
|
|
1682
|
+
// Do not surface SDK/config error text: it may contain credential material.
|
|
1683
|
+
throw new Error("MODEL_CONFIG_REFRESH_FAILED");
|
|
1684
|
+
}
|
|
1685
|
+
if (this.disposedSessions.has(sessionId) ||
|
|
1686
|
+
this.sessions.get(sessionId) !== session) {
|
|
1687
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
1688
|
+
}
|
|
1689
|
+
configFile.version = nextVersion;
|
|
1690
|
+
this.bumpRevision(sessionId);
|
|
1691
|
+
return true;
|
|
1576
1692
|
}
|
|
1577
1693
|
/**
|
|
1578
1694
|
* Apply a thinking-level selection. `auto` records the selection WITHOUT
|
|
@@ -1695,6 +1811,8 @@ export class ConsolePiRuntime {
|
|
|
1695
1811
|
};
|
|
1696
1812
|
}
|
|
1697
1813
|
const busyReasons = [];
|
|
1814
|
+
if (this.promptOwners.has(sessionId))
|
|
1815
|
+
busyReasons.push("prompt in flight");
|
|
1698
1816
|
if (session.isStreaming === true || session.isIdle === false)
|
|
1699
1817
|
busyReasons.push("streaming");
|
|
1700
1818
|
if (session.isCompacting === true)
|
|
@@ -1726,6 +1844,7 @@ export class ConsolePiRuntime {
|
|
|
1726
1844
|
const services = this.serviceScopes.get(sessionId);
|
|
1727
1845
|
if (services)
|
|
1728
1846
|
this.applySessionCompactionOverride(sessionId, services, session);
|
|
1847
|
+
await this.refreshModelConfiguration(sessionId, "force");
|
|
1729
1848
|
}
|
|
1730
1849
|
catch (error) {
|
|
1731
1850
|
return {
|
|
@@ -1737,7 +1856,6 @@ export class ConsolePiRuntime {
|
|
|
1737
1856
|
finally {
|
|
1738
1857
|
this.reloadInFlight.delete(sessionId);
|
|
1739
1858
|
}
|
|
1740
|
-
this.bumpRevision(sessionId);
|
|
1741
1859
|
// Reload replaces the effective resource/session context. Persisted Goal
|
|
1742
1860
|
// remains visible, but continuation authorization must be renewed by user.
|
|
1743
1861
|
this.options.goalRegistry?.disarm(sessionId);
|
|
@@ -2203,6 +2321,16 @@ export class ConsolePiRuntime {
|
|
|
2203
2321
|
});
|
|
2204
2322
|
}
|
|
2205
2323
|
catch (error) {
|
|
2324
|
+
const code = error && typeof error === "object" && "code" in error
|
|
2325
|
+
? String(error.code ?? "")
|
|
2326
|
+
: "";
|
|
2327
|
+
if (code === "SESSION_FILE_MISSING" || code === "ENOENT") {
|
|
2328
|
+
return {
|
|
2329
|
+
ok: false,
|
|
2330
|
+
code: "NO_SESSION_FILE",
|
|
2331
|
+
message: `Pi session file missing or not openable: ${init.sessionFile}`,
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2206
2334
|
return {
|
|
2207
2335
|
ok: false,
|
|
2208
2336
|
code: "REOPEN_FAILED",
|
|
@@ -2256,6 +2384,7 @@ export class ConsolePiRuntime {
|
|
|
2256
2384
|
this.sessions.set(sessionId, session);
|
|
2257
2385
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
2258
2386
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
2387
|
+
this.modelConfigFiles.set(sessionId, built.modelConfigFile);
|
|
2259
2388
|
this.sessionModels.set(sessionId, init.model);
|
|
2260
2389
|
this.serviceScopes.set(sessionId, built.services);
|
|
2261
2390
|
this.revisions.set(sessionId, 1);
|
|
@@ -2309,6 +2438,7 @@ export class ConsolePiRuntime {
|
|
|
2309
2438
|
*/
|
|
2310
2439
|
async buildSessionWithServices(init) {
|
|
2311
2440
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
2441
|
+
const modelConfigFile = await captureModelConfigFile(agentDir);
|
|
2312
2442
|
const appendSystemPrompt = [
|
|
2313
2443
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
2314
2444
|
...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
@@ -2342,6 +2472,7 @@ export class ConsolePiRuntime {
|
|
|
2342
2472
|
return {
|
|
2343
2473
|
session,
|
|
2344
2474
|
modelRuntime,
|
|
2475
|
+
modelConfigFile,
|
|
2345
2476
|
services: {
|
|
2346
2477
|
resourceLoader: services.resourceLoader,
|
|
2347
2478
|
settingsManager: services.settingsManager,
|
|
@@ -2353,14 +2484,31 @@ export class ConsolePiRuntime {
|
|
|
2353
2484
|
return this.sessions.get(sessionId);
|
|
2354
2485
|
}
|
|
2355
2486
|
/** Inject one queued message into the currently running Pi turn. */
|
|
2356
|
-
async steer(sessionId, text, images) {
|
|
2487
|
+
async steer(sessionId, clientRequestId, text, images) {
|
|
2357
2488
|
const session = this.sessions.get(sessionId);
|
|
2358
2489
|
if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
|
|
2359
2490
|
throw new Error("CHAT_TURN_NOT_ACTIVE");
|
|
2360
2491
|
}
|
|
2361
2492
|
if (typeof session.steer !== "function")
|
|
2362
2493
|
throw new Error("CHAT_STEER_UNAVAILABLE");
|
|
2363
|
-
|
|
2494
|
+
const pending = this.pendingSteering.get(sessionId) ?? [];
|
|
2495
|
+
const identity = { clientRequestId, text };
|
|
2496
|
+
pending.push(identity);
|
|
2497
|
+
this.pendingSteering.set(sessionId, pending);
|
|
2498
|
+
try {
|
|
2499
|
+
await session.steer(text, images);
|
|
2500
|
+
}
|
|
2501
|
+
catch (error) {
|
|
2502
|
+
const current = this.pendingSteering.get(sessionId);
|
|
2503
|
+
if (current) {
|
|
2504
|
+
const index = current.indexOf(identity);
|
|
2505
|
+
if (index >= 0)
|
|
2506
|
+
current.splice(index, 1);
|
|
2507
|
+
if (current.length === 0)
|
|
2508
|
+
this.pendingSteering.delete(sessionId);
|
|
2509
|
+
}
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2364
2512
|
}
|
|
2365
2513
|
hasSession(sessionId) {
|
|
2366
2514
|
return this.sessions.has(sessionId);
|
|
@@ -2396,12 +2544,13 @@ export class ConsolePiRuntime {
|
|
|
2396
2544
|
*/
|
|
2397
2545
|
async prompt(sessionId, text, onEvent, options) {
|
|
2398
2546
|
// Synchronous admission fence — before any await.
|
|
2399
|
-
if (this.promptOwners.has(sessionId)) {
|
|
2547
|
+
if (this.promptOwners.has(sessionId) || this.reloadInFlight.has(sessionId)) {
|
|
2400
2548
|
throw new Error("CHAT_TURN_ACTIVE");
|
|
2401
2549
|
}
|
|
2402
2550
|
this.promptOwners.add(sessionId);
|
|
2403
2551
|
let unsub;
|
|
2404
2552
|
let abortPromise;
|
|
2553
|
+
let initialUserMessageStarted = false;
|
|
2405
2554
|
const onAbort = () => {
|
|
2406
2555
|
// M1 T08 / AC-R3-003: Stop must abort only the current turn, NEVER
|
|
2407
2556
|
// dispose the session. Await session.abort() so ownership settles
|
|
@@ -2458,6 +2607,11 @@ export class ConsolePiRuntime {
|
|
|
2458
2607
|
if (options?.signal?.aborted) {
|
|
2459
2608
|
return;
|
|
2460
2609
|
}
|
|
2610
|
+
await this.refreshModelConfiguration(sessionId, "if-changed");
|
|
2611
|
+
// Stop may arrive while stat/refresh/rebind is in flight.
|
|
2612
|
+
if (options?.signal?.aborted) {
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2461
2615
|
const emitContextUsage = () => {
|
|
2462
2616
|
try {
|
|
2463
2617
|
const contextUsage = session.getSessionStats?.()?.contextUsage;
|
|
@@ -2499,6 +2653,27 @@ export class ConsolePiRuntime {
|
|
|
2499
2653
|
};
|
|
2500
2654
|
unsub = session.subscribe((event) => {
|
|
2501
2655
|
const observedAt = Date.now();
|
|
2656
|
+
if (event.type === "message_start" && isUserSdkMessage(event.message)) {
|
|
2657
|
+
if (!initialUserMessageStarted) {
|
|
2658
|
+
// The first user message_start belongs to session.prompt(text), not
|
|
2659
|
+
// to a steering request that may have raced with prompt startup.
|
|
2660
|
+
initialUserMessageStarted = true;
|
|
2661
|
+
}
|
|
2662
|
+
else {
|
|
2663
|
+
const pending = this.pendingSteering.get(sessionId);
|
|
2664
|
+
const accepted = pending?.shift();
|
|
2665
|
+
if (pending?.length === 0)
|
|
2666
|
+
this.pendingSteering.delete(sessionId);
|
|
2667
|
+
if (accepted) {
|
|
2668
|
+
onEvent({
|
|
2669
|
+
type: "steering_accepted",
|
|
2670
|
+
sessionId,
|
|
2671
|
+
clientRequestId: accepted.clientRequestId,
|
|
2672
|
+
text: accepted.text,
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2502
2677
|
if (event.type === "compaction_start") {
|
|
2503
2678
|
activeCompaction = {
|
|
2504
2679
|
compactionId: newCompactionId(sessionId),
|
|
@@ -2637,6 +2812,10 @@ export class ConsolePiRuntime {
|
|
|
2637
2812
|
finally {
|
|
2638
2813
|
if (this.options.questionRegistry)
|
|
2639
2814
|
this.options.questionRegistry.clearTurn(sessionId);
|
|
2815
|
+
// Unconsumed durable DeferredTurns remain dispatching and are recovered by
|
|
2816
|
+
// the route layer after this active Turn closes. Never carry their volatile
|
|
2817
|
+
// identities into a later prompt.
|
|
2818
|
+
this.pendingSteering.delete(sessionId);
|
|
2640
2819
|
this.promptOwners.delete(sessionId);
|
|
2641
2820
|
}
|
|
2642
2821
|
}
|
|
@@ -2758,11 +2937,13 @@ export class ConsolePiRuntime {
|
|
|
2758
2937
|
this.sessionManagers.delete(sessionId);
|
|
2759
2938
|
this.mainlineLeaves.delete(sessionId);
|
|
2760
2939
|
this.modelRuntimes.delete(sessionId);
|
|
2940
|
+
this.modelConfigFiles.delete(sessionId);
|
|
2761
2941
|
this.sessionModels.delete(sessionId);
|
|
2762
2942
|
this.serviceScopes.delete(sessionId);
|
|
2763
2943
|
this.revisions.delete(sessionId);
|
|
2764
2944
|
this.thinkingSelections.delete(sessionId);
|
|
2765
2945
|
this.reloadInFlight.delete(sessionId);
|
|
2946
|
+
this.pendingSteering.delete(sessionId);
|
|
2766
2947
|
}
|
|
2767
2948
|
disposeAll() {
|
|
2768
2949
|
for (const id of [...this.sessions.keys()])
|
|
@@ -2795,6 +2976,11 @@ function mapAssistantMessageEvent(type, sessionId, message) {
|
|
|
2795
2976
|
...(content.length > 0 ? { content } : {}),
|
|
2796
2977
|
};
|
|
2797
2978
|
}
|
|
2979
|
+
function isUserSdkMessage(message) {
|
|
2980
|
+
return Boolean(message &&
|
|
2981
|
+
typeof message === "object" &&
|
|
2982
|
+
message.role === "user");
|
|
2983
|
+
}
|
|
2798
2984
|
function mapSdkEvent(sessionId, event, compactionId) {
|
|
2799
2985
|
switch (event.type) {
|
|
2800
2986
|
case "agent_start":
|
|
@@ -2980,6 +3166,36 @@ export function createDefaultPiSdkBindings() {
|
|
|
2980
3166
|
id: opts.sessionId,
|
|
2981
3167
|
});
|
|
2982
3168
|
},
|
|
3169
|
+
materializeSessionShell: async (opts) => {
|
|
3170
|
+
const sdk = (await loadSdk());
|
|
3171
|
+
const manager = opts.sessionManager;
|
|
3172
|
+
if (typeof manager.setSessionFile !== "function") {
|
|
3173
|
+
throw new Error("Pi SessionManager cannot materialize a session file");
|
|
3174
|
+
}
|
|
3175
|
+
let created = false;
|
|
3176
|
+
try {
|
|
3177
|
+
await mkdir(path.dirname(opts.sessionFile), { recursive: true });
|
|
3178
|
+
// Match Pi's documented session header. Rebinding through the public
|
|
3179
|
+
// SessionManager API marks it flushed, so Pi appends the first user
|
|
3180
|
+
// entry instead of trying to create the already-present file later.
|
|
3181
|
+
await writeFile(opts.sessionFile, `${JSON.stringify({
|
|
3182
|
+
type: "session",
|
|
3183
|
+
version: typeof sdk.CURRENT_SESSION_VERSION === "number"
|
|
3184
|
+
? sdk.CURRENT_SESSION_VERSION
|
|
3185
|
+
: 3,
|
|
3186
|
+
id: opts.sessionId,
|
|
3187
|
+
timestamp: new Date().toISOString(),
|
|
3188
|
+
cwd: opts.cwd,
|
|
3189
|
+
})}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
3190
|
+
created = true;
|
|
3191
|
+
manager.setSessionFile(opts.sessionFile);
|
|
3192
|
+
}
|
|
3193
|
+
catch (error) {
|
|
3194
|
+
if (created)
|
|
3195
|
+
await rm(opts.sessionFile, { force: true }).catch(() => undefined);
|
|
3196
|
+
throw error;
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
2983
3199
|
createInMemorySessionManager: async (opts) => {
|
|
2984
3200
|
const sdk = (await loadSdk());
|
|
2985
3201
|
return sdk.SessionManager.inMemory(opts.cwd);
|
|
@@ -2996,13 +3212,19 @@ export function createDefaultPiSdkBindings() {
|
|
|
2996
3212
|
// wrong conversation under the durable Console sessionId. Fail closed
|
|
2997
3213
|
// before open when the persisted file is absent.
|
|
2998
3214
|
await access(opts.sessionFile);
|
|
2999
|
-
return sdk.SessionManager.open(opts.sessionFile);
|
|
3000
3215
|
}
|
|
3001
|
-
catch {
|
|
3002
|
-
|
|
3003
|
-
|
|
3216
|
+
catch (error) {
|
|
3217
|
+
const code = error && typeof error === "object" && "code" in error
|
|
3218
|
+
? String(error.code ?? "")
|
|
3219
|
+
: "";
|
|
3220
|
+
if (code === "ENOENT") {
|
|
3221
|
+
throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
|
|
3222
|
+
}
|
|
3223
|
+
// File exists but is unreadable (permissions / I/O) — caller maps
|
|
3224
|
+
// undefined to 503 rather than silently creating a fresh session.
|
|
3004
3225
|
return undefined;
|
|
3005
3226
|
}
|
|
3227
|
+
return sdk.SessionManager.open(opts.sessionFile);
|
|
3006
3228
|
},
|
|
3007
3229
|
resolveModel: (opts) => {
|
|
3008
3230
|
const rt = opts.modelRuntime;
|
|
@@ -9,6 +9,9 @@ const TREE_LIMIT = 500;
|
|
|
9
9
|
export const TEXT_PREVIEW_LIMIT = 512 * 1024;
|
|
10
10
|
/** Full-file ceiling for the sandboxed HTML preview route and its assets. */
|
|
11
11
|
export const HTML_PREVIEW_ASSET_LIMIT = 20 * 1024 * 1024;
|
|
12
|
+
/** Self-contained HTML exports commonly embed data and can be much larger than
|
|
13
|
+
* an individual image/script asset. Keep a separate, still-bounded ceiling. */
|
|
14
|
+
export const HTML_PREVIEW_DOCUMENT_LIMIT = 64 * 1024 * 1024;
|
|
12
15
|
export const REPO_HTML_PREVIEW_ROUTE_PREFIX = "/api/operator/v1/chat/repo/html/";
|
|
13
16
|
const SKIP_DIRS = new Set([
|
|
14
17
|
".git",
|
|
@@ -190,8 +193,12 @@ export async function readRepoHtmlAsset(repoRoot, rawPath) {
|
|
|
190
193
|
const info = await handle.stat();
|
|
191
194
|
if (!info.isFile())
|
|
192
195
|
throw new RepoBrowserError(400, "NOT_A_FILE", "path is not a regular file");
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
const contentType = repoHtmlAssetContentType(guard.repoRelative);
|
|
197
|
+
const limit = contentType.startsWith("text/html")
|
|
198
|
+
? HTML_PREVIEW_DOCUMENT_LIMIT
|
|
199
|
+
: HTML_PREVIEW_ASSET_LIMIT;
|
|
200
|
+
if (info.size > limit)
|
|
201
|
+
throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${limit} byte HTML preview limit`);
|
|
195
202
|
const rechecked = await guardReadPath(repoRoot, rawPath);
|
|
196
203
|
if (!rechecked.ok)
|
|
197
204
|
throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
|
|
@@ -202,7 +209,7 @@ export async function readRepoHtmlAsset(repoRoot, rawPath) {
|
|
|
202
209
|
throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
|
|
203
210
|
return {
|
|
204
211
|
body: await handle.readFile(),
|
|
205
|
-
contentType
|
|
212
|
+
contentType,
|
|
206
213
|
bytes: info.size,
|
|
207
214
|
};
|
|
208
215
|
}
|