@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -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/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 +176 -9
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +42 -22
- 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 +117 -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 +0 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-D7oU983t.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BrKSRdFS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-CiXV7L56.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-D_Hs_gIG.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-DbM-Lqqh.js} +1 -1
- package/dist/worker/console/static/assets/channel-D1xajGpe.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-D1M8uqJw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-4D3RFHT6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CdwpY_n9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-BLeLZ9PG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CeRNEMvc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-CjkHP8Vy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-B54ZUQY-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-4bKaMVmx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-Cyjfznu_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CDN-zGFV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-HxJmz411.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-DtSt5K2w.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-VtkSpRmG.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-Bj4DT4du.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-Bpuk6BmP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-BHYGv_yO.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-DEx3WwnT.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-Cj0dS5e8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-B97h8l21.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-DMSFAhBd.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-DhVOCbt6.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-GlakITWL.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-VxPpu9eQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js} +1 -1
- package/dist/worker/console/static/assets/index-BLzcAykx.js +451 -0
- package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-BXYuU4Uw.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-C34erCbU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-DZCAiOU3.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-C9gaTdUm.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CvI0Z17U.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-DMMSIx7j.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-BKKP0EO6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-C3LiFbl5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-DNthMjzc.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-miFsr4Tv.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-yXUncmn6.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-DLeWKLak.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-CI3he1R3.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-B7jfDkS5.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BmrZod7J.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-CDOlHVSY.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-Cz-XX1gy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-caV4cI8C.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-dImPlnIO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CtRYVJcn.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/workspace-context.js +1 -111
- package/dist/worker/observability/read-model.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
|
@@ -175,11 +175,8 @@ export function splitDocumentIntoFragments(doc) {
|
|
|
175
175
|
const explicitSectionAc = matchExplicitAcHeading(lastHeadingSegment);
|
|
176
176
|
const sectionKind = classifySectionHeading(section.headingPath);
|
|
177
177
|
const effectiveKind = explicitSectionAc ? "acceptance" : sectionKind;
|
|
178
|
-
// V4 显式 AC 标题行已在下方入口 emit(explicitSectionAc 分支);正文循环
|
|
179
|
-
// 必须从标题行的下一行开始,否则同一标题行会被正文循环的 heading 分支
|
|
180
|
-
// 再次 emit,产生结构性 duplicate(每个 AC 标题式片段出现两次)。
|
|
181
178
|
const bodyStart = section.headingPath
|
|
182
|
-
? section.startLine + 1
|
|
179
|
+
? section.startLine + (explicitSectionAc ? 0 : 1)
|
|
183
180
|
: section.startLine;
|
|
184
181
|
const bodyEnd = section.endLine;
|
|
185
182
|
const bodyLines = lines.slice(Math.max(0, bodyStart - 1), Math.min(bodyEnd, lines.length));
|
|
@@ -441,17 +438,14 @@ export function buildFragmentInventory(input) {
|
|
|
441
438
|
disposition = "non-goal";
|
|
442
439
|
}
|
|
443
440
|
else {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// this branch is always contextual.
|
|
453
|
-
disposition = "context-only";
|
|
454
|
-
reason = "context-only fragment; excluded from canonical requirement coverage";
|
|
441
|
+
const executable = EXECUTABLE_HINT_RE.test(text);
|
|
442
|
+
disposition = executable ? "unresolved" : "context-only";
|
|
443
|
+
if (disposition === "unresolved") {
|
|
444
|
+
reason = "executable fragment outside recognized requirement/acceptance section";
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
reason = "context-only fragment; excluded from canonical requirement coverage";
|
|
448
|
+
}
|
|
455
449
|
}
|
|
456
450
|
const priorId = seenText.get(text);
|
|
457
451
|
if (priorId) {
|
|
@@ -5,9 +5,6 @@ import { ContractMutationError, applyTaskContract, logicalRevisionForState, obse
|
|
|
5
5
|
import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
|
|
6
6
|
import { getTaskContractPaths } from "../contract/paths.js";
|
|
7
7
|
import { loadTaskConfig } from "../runtime.js";
|
|
8
|
-
import { discoverFrontendProjectCapability, resolveFrontendSpecRootAliases, } from "../frontend-project-capability.js";
|
|
9
|
-
import { classifyTaskDemand } from "../task-demand-routing.js";
|
|
10
|
-
import { DEFAULT_FRONTEND_SPEC_ROOTS, extractDeclaredFrontendSpecRoots, } from "../../shared/openspec-spec.js";
|
|
11
8
|
import { buildPrepareDraft } from "./build-draft.js";
|
|
12
9
|
import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, appendLedgerGaps, } from "./completeness.js";
|
|
13
10
|
import { detectProductArtifactMeta, isIntakeSoftGapCode, } from "./artifact-meta.js";
|
|
@@ -684,7 +681,7 @@ export async function prepareTaskSource(input) {
|
|
|
684
681
|
facts = importedFacts ?? mergedSupplemental;
|
|
685
682
|
}
|
|
686
683
|
}
|
|
687
|
-
|
|
684
|
+
const built = buildPrepareDraft({
|
|
688
685
|
taskId,
|
|
689
686
|
existingTaskConfig,
|
|
690
687
|
baseDraft,
|
|
@@ -692,91 +689,6 @@ export async function prepareTaskSource(input) {
|
|
|
692
689
|
flags: input.flags,
|
|
693
690
|
references,
|
|
694
691
|
});
|
|
695
|
-
// Resolve an implicit frontend workflow before the first managed contract is
|
|
696
|
-
// projected. Explicit taskKind (including explicit standard) remains a hard
|
|
697
|
-
// contract; only a standard task with strong source/path/project evidence may
|
|
698
|
-
// be promoted here. This keeps task.json, contract.json, and the first DAG
|
|
699
|
-
// generation on the same workflow without a later hash-changing re-projection.
|
|
700
|
-
if (built.draft.taskKind === "standard" &&
|
|
701
|
-
!built.draft.taskKindExplicit) {
|
|
702
|
-
const routingRequirement = [
|
|
703
|
-
...(sourceIntegrity?.documents ?? [])
|
|
704
|
-
.map((document) => document.content)
|
|
705
|
-
.filter((content) => Boolean(content?.trim())),
|
|
706
|
-
...(input.intent.kind === "facts" && input.intent.text
|
|
707
|
-
? [input.intent.text]
|
|
708
|
-
: []),
|
|
709
|
-
built.draft.requirement.objective,
|
|
710
|
-
...built.draft.requirement.scope,
|
|
711
|
-
...built.draft.requirement.acceptanceCriteria.map((item) => item.text),
|
|
712
|
-
].join("\n");
|
|
713
|
-
let frontendProject;
|
|
714
|
-
try {
|
|
715
|
-
frontendProject = await discoverFrontendProjectCapability(repoRoot);
|
|
716
|
-
}
|
|
717
|
-
catch {
|
|
718
|
-
// Capability discovery is advisory for routing. A failed probe must not
|
|
719
|
-
// turn an otherwise valid standard contract preparation into an error.
|
|
720
|
-
}
|
|
721
|
-
const classification = classifyTaskDemand({
|
|
722
|
-
title: built.draft.title,
|
|
723
|
-
requirementMarkdown: routingRequirement,
|
|
724
|
-
allowedPaths: built.draft.constraints.allowedPaths,
|
|
725
|
-
frontendProject: frontendProject
|
|
726
|
-
? {
|
|
727
|
-
framework: frontendProject.framework,
|
|
728
|
-
evidencePaths: frontendProject.evidencePaths,
|
|
729
|
-
}
|
|
730
|
-
: undefined,
|
|
731
|
-
});
|
|
732
|
-
if (classification.kind === "frontend") {
|
|
733
|
-
built = buildPrepareDraft({
|
|
734
|
-
taskId,
|
|
735
|
-
existingTaskConfig,
|
|
736
|
-
baseDraft,
|
|
737
|
-
facts,
|
|
738
|
-
flags: {
|
|
739
|
-
...input.flags,
|
|
740
|
-
taskKind: "frontend-implementation",
|
|
741
|
-
},
|
|
742
|
-
references,
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
// A user can explicitly name a non-default normative directory in the task
|
|
747
|
-
// prompt (for example “规范目录 `design-system`”). Freeze that root into
|
|
748
|
-
// the managed task config before the DAG exists; normal DAG selection consumes
|
|
749
|
-
// the already-resolved task kind instead of mutating task.json later.
|
|
750
|
-
if (built.draft.taskKind === "frontend-implementation") {
|
|
751
|
-
// An imported PRD is materialized under sourceIntegrity rather than copied
|
|
752
|
-
// into intent.text. Use both inputs so a `--prd` invocation can declare a
|
|
753
|
-
// task-owned spec root just like supplemental free-form text can.
|
|
754
|
-
const sourceText = [
|
|
755
|
-
...(sourceIntegrity?.documents ?? []).flatMap((document) => document.content ? [document.content] : []),
|
|
756
|
-
...(input.intent.kind === "facts" && input.intent.text
|
|
757
|
-
? [input.intent.text]
|
|
758
|
-
: []),
|
|
759
|
-
].join("\n");
|
|
760
|
-
const inferredRoots = extractDeclaredFrontendSpecRoots(sourceText);
|
|
761
|
-
if (inferredRoots.length > 0) {
|
|
762
|
-
const current = existingTaskConfig?.frontendOpenspec;
|
|
763
|
-
const configuredRoots = current?.specRoots ?? [...DEFAULT_FRONTEND_SPEC_ROOTS];
|
|
764
|
-
// Include roots declared by this source before resolving physical-target
|
|
765
|
-
// aliases. A newly declared root may itself be a symlink (for example
|
|
766
|
-
// `project-specs -> .../ai_doc`), so resolving aliases against only the
|
|
767
|
-
// previous config cannot map `ai_doc/rules.md` back to project-specs.
|
|
768
|
-
const provisionalRoots = [
|
|
769
|
-
...new Set([...configuredRoots, ...inferredRoots]),
|
|
770
|
-
].sort();
|
|
771
|
-
const aliases = await resolveFrontendSpecRootAliases(repoRoot, provisionalRoots);
|
|
772
|
-
const aliasMap = new Map(aliases.map(({ alias, logicalRoot }) => [alias, logicalRoot]));
|
|
773
|
-
built.draft.frontendOpenspec = {
|
|
774
|
-
policy: current?.policy ?? "cited",
|
|
775
|
-
specRoots: [...new Set([...configuredRoots, ...inferredRoots.map((root) => aliasMap.get(root) ?? root)])].sort(),
|
|
776
|
-
requiredReadPaths: current?.requiredReadPaths ?? [],
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
692
|
const { gaps, risks } = listPrepareGaps({
|
|
781
693
|
draft: built.draft,
|
|
782
694
|
state,
|
|
@@ -457,12 +457,8 @@ async function buildPrompt(input) {
|
|
|
457
457
|
"Do NOT invent product behavior that is not supported by the documents.",
|
|
458
458
|
"You MAY recommend an optional engineering block when the repo layout hint supports it:",
|
|
459
459
|
"- allowedPaths / forbiddenPaths as minimal precise globs (e.g. source/packages/**/*.test.ts)",
|
|
460
|
-
"- verifyCommands as real package scripts (e.g. pnpm -C source test)
|
|
461
|
-
"- taskKind when clearly implied
|
|
462
|
-
" - frontend-implementation: the deliverable is a frontend component/page/UI feature (even when it ships with its own tests)",
|
|
463
|
-
" - frontend-test: the deliverable is a browser/UI automation test suite for EXISTING functionality (no implementation work)",
|
|
464
|
-
" - backend-test: pytest/backend test suite; standard: general repo implementation",
|
|
465
|
-
" A task that IMPLEMENTS a component and adds tests for it is frontend-implementation, never frontend-test.",
|
|
460
|
+
"- verifyCommands as real package scripts (e.g. pnpm -C source test)",
|
|
461
|
+
"- taskKind when clearly implied (backend-test / frontend-test / standard / …)",
|
|
466
462
|
"Prefer smallest correct write boundary; do not invent product paths that are not in the layout hint.",
|
|
467
463
|
"Do NOT rewrite or suggest editing the original PRD files.",
|
|
468
464
|
"Prefer explicit text; use provenance derived for rephrasing; use inferred only when clearly implied — if you must invent ACs, leave acceptanceCriteria empty so the system can stop for clarification.",
|
|
@@ -22,18 +22,6 @@ function slugifyName(value) {
|
|
|
22
22
|
.replace(/-{2,}/g, "-");
|
|
23
23
|
return slug || "requirement";
|
|
24
24
|
}
|
|
25
|
-
/** Stable identity of a manifest document for change detection. Timestamps and
|
|
26
|
-
* byte sizes are excluded: identical content re-imported later must not appear
|
|
27
|
-
* "changed" (the canonical hash includes manifest bytes). */
|
|
28
|
-
function documentIdentity(doc) {
|
|
29
|
-
return `${doc.role}|${doc.name}|${doc.materializedPath}|${doc.sha256}`;
|
|
30
|
-
}
|
|
31
|
-
function sameDocumentSet(left, right) {
|
|
32
|
-
if (left.length !== right.length)
|
|
33
|
-
return false;
|
|
34
|
-
const rightIds = new Set(right.map(documentIdentity));
|
|
35
|
-
return left.every((doc) => rightIds.has(documentIdentity(doc)));
|
|
36
|
-
}
|
|
37
25
|
function ensureUniqueReferenceName(existing, baseName, extension) {
|
|
38
26
|
const used = new Set(existing);
|
|
39
27
|
const candidate = `${baseName}${extension}`;
|
|
@@ -161,19 +149,10 @@ export async function importPrdDocument(input) {
|
|
|
161
149
|
const documents = [...(existingManifest?.documents ?? [])].filter((entry) => entry.materializedPath !== materializedRelativePath);
|
|
162
150
|
documents.push(document);
|
|
163
151
|
documents.sort((left, right) => left.materializedPath.localeCompare(right.materializedPath));
|
|
164
|
-
// Keep `updatedAt` stable across re-imports of the same content: the
|
|
165
|
-
// canonical contract hash includes the manifest bytes, so a fresh
|
|
166
|
-
// timestamp on every identical re-import would make the observed
|
|
167
|
-
// canonical hash drift and fail task verification. Only bump it when the
|
|
168
|
-
// document set actually changed (a new/changed materialized file).
|
|
169
|
-
const documentSetChanged = !existingManifest ||
|
|
170
|
-
!sameDocumentSet(existingManifest.documents, documents);
|
|
171
152
|
const manifest = {
|
|
172
153
|
schemaVersion: 1,
|
|
173
154
|
taskId: input.taskId,
|
|
174
|
-
updatedAt:
|
|
175
|
-
? importedAt
|
|
176
|
-
: (existingManifest?.updatedAt ?? importedAt),
|
|
155
|
+
updatedAt: importedAt,
|
|
177
156
|
documents,
|
|
178
157
|
};
|
|
179
158
|
await writeSourceManifest(paths.sourceDir, manifest);
|
|
@@ -19,7 +19,9 @@ export function shouldPlayCompletionSound(input) {
|
|
|
19
19
|
export function copyTextForMessage(input) {
|
|
20
20
|
return input.code ?? input.text;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/** Project HTML may run its own UI scripts, but remains an opaque origin with
|
|
23
|
+
* forms, popups and top-level navigation denied by omission. */
|
|
24
|
+
export const HTML_PREVIEW_SANDBOX = "allow-scripts";
|
|
23
25
|
export function previewSandbox() {
|
|
24
|
-
return { sandbox:
|
|
26
|
+
return { sandbox: HTML_PREVIEW_SANDBOX };
|
|
25
27
|
}
|
|
@@ -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, stat } 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). */
|
|
@@ -1340,6 +1383,9 @@ export class ConsolePiRuntime {
|
|
|
1340
1383
|
if (this.disposedSessions.has(sessionId))
|
|
1341
1384
|
return;
|
|
1342
1385
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
1386
|
+
// Capture before services parse models.json. A concurrent edit can therefore
|
|
1387
|
+
// cause at most one harmless extra refresh, never a missed next-turn refresh.
|
|
1388
|
+
const modelConfigFile = await captureModelConfigFile(agentDir);
|
|
1343
1389
|
const appendSystemPrompt = [
|
|
1344
1390
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
1345
1391
|
...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
@@ -1399,6 +1445,7 @@ export class ConsolePiRuntime {
|
|
|
1399
1445
|
this.sessions.set(sessionId, session);
|
|
1400
1446
|
this.sessionManagers.set(sessionId, init.sessionManager);
|
|
1401
1447
|
this.modelRuntimes.set(sessionId, modelRuntime);
|
|
1448
|
+
this.modelConfigFiles.set(sessionId, modelConfigFile);
|
|
1402
1449
|
this.sessionModels.set(sessionId, model);
|
|
1403
1450
|
this.serviceScopes.set(sessionId, {
|
|
1404
1451
|
resourceLoader: services.resourceLoader,
|
|
@@ -1419,6 +1466,7 @@ export class ConsolePiRuntime {
|
|
|
1419
1466
|
this.sessions.delete(sessionId);
|
|
1420
1467
|
this.sessionManagers.delete(sessionId);
|
|
1421
1468
|
this.modelRuntimes.delete(sessionId);
|
|
1469
|
+
this.modelConfigFiles.delete(sessionId);
|
|
1422
1470
|
this.sessionModels.delete(sessionId);
|
|
1423
1471
|
this.serviceScopes.delete(sessionId);
|
|
1424
1472
|
this.revisions.delete(sessionId);
|
|
@@ -1497,6 +1545,7 @@ export class ConsolePiRuntime {
|
|
|
1497
1545
|
this.sessions.set(sessionId, built.session);
|
|
1498
1546
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
1499
1547
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
1548
|
+
this.modelConfigFiles.set(sessionId, built.modelConfigFile);
|
|
1500
1549
|
this.sessionModels.set(sessionId, init.model);
|
|
1501
1550
|
this.serviceScopes.set(sessionId, built.services);
|
|
1502
1551
|
this.revisions.set(sessionId, 1);
|
|
@@ -1568,11 +1617,69 @@ export class ConsolePiRuntime {
|
|
|
1568
1617
|
if (!resolved)
|
|
1569
1618
|
throw new Error("MODEL_NOT_AVAILABLE");
|
|
1570
1619
|
await session.setModel(resolved);
|
|
1571
|
-
|
|
1572
|
-
return {
|
|
1620
|
+
const applied = {
|
|
1573
1621
|
provider: session.model?.provider ?? model.provider,
|
|
1574
1622
|
modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
|
|
1575
1623
|
};
|
|
1624
|
+
this.sessionModels.set(sessionId, applied);
|
|
1625
|
+
this.bumpRevision(sessionId);
|
|
1626
|
+
return applied;
|
|
1627
|
+
}
|
|
1628
|
+
async refreshModelConfiguration(sessionId, mode) {
|
|
1629
|
+
const session = this.sessions.get(sessionId);
|
|
1630
|
+
const modelRuntime = this.modelRuntimes.get(sessionId);
|
|
1631
|
+
const configFile = this.modelConfigFiles.get(sessionId);
|
|
1632
|
+
if (!session || !modelRuntime || !configFile) {
|
|
1633
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
1634
|
+
}
|
|
1635
|
+
const nextVersion = await readModelConfigFileVersion(configFile.path);
|
|
1636
|
+
if (mode === "if-changed" &&
|
|
1637
|
+
sameModelConfigFileVersion(configFile.version, nextVersion)) {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
if (typeof modelRuntime.refresh !== "function") {
|
|
1641
|
+
throw new Error("MODEL_CONFIG_REFRESH_UNSUPPORTED");
|
|
1642
|
+
}
|
|
1643
|
+
const stored = this.sessionModels.get(sessionId);
|
|
1644
|
+
const provider = session.model?.provider ?? stored?.provider;
|
|
1645
|
+
const modelId = session.model?.id ?? session.model?.modelId ?? stored?.modelId;
|
|
1646
|
+
try {
|
|
1647
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
1648
|
+
if (typeof modelRuntime.getError === "function" && modelRuntime.getError()) {
|
|
1649
|
+
throw new Error("MODEL_CONFIG_REFRESH_FAILED");
|
|
1650
|
+
}
|
|
1651
|
+
if (provider && modelId) {
|
|
1652
|
+
if (typeof session.setModel !== "function") {
|
|
1653
|
+
throw new Error("MODEL_CONFIG_REBIND_UNSUPPORTED");
|
|
1654
|
+
}
|
|
1655
|
+
const resolved = this.bindings.resolveModel({
|
|
1656
|
+
modelRuntime,
|
|
1657
|
+
provider,
|
|
1658
|
+
modelId,
|
|
1659
|
+
});
|
|
1660
|
+
if (!resolved) {
|
|
1661
|
+
throw new Error("MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH");
|
|
1662
|
+
}
|
|
1663
|
+
await session.setModel(resolved);
|
|
1664
|
+
this.sessionModels.set(sessionId, { provider, modelId });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
if (error instanceof Error &&
|
|
1669
|
+
(error.message === "MODEL_CONFIG_REBIND_UNSUPPORTED" ||
|
|
1670
|
+
error.message === "MODEL_NOT_AVAILABLE_AFTER_CONFIG_REFRESH")) {
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1673
|
+
// Do not surface SDK/config error text: it may contain credential material.
|
|
1674
|
+
throw new Error("MODEL_CONFIG_REFRESH_FAILED");
|
|
1675
|
+
}
|
|
1676
|
+
if (this.disposedSessions.has(sessionId) ||
|
|
1677
|
+
this.sessions.get(sessionId) !== session) {
|
|
1678
|
+
throw new Error(`chat session not active: ${sessionId}`);
|
|
1679
|
+
}
|
|
1680
|
+
configFile.version = nextVersion;
|
|
1681
|
+
this.bumpRevision(sessionId);
|
|
1682
|
+
return true;
|
|
1576
1683
|
}
|
|
1577
1684
|
/**
|
|
1578
1685
|
* Apply a thinking-level selection. `auto` records the selection WITHOUT
|
|
@@ -1695,6 +1802,8 @@ export class ConsolePiRuntime {
|
|
|
1695
1802
|
};
|
|
1696
1803
|
}
|
|
1697
1804
|
const busyReasons = [];
|
|
1805
|
+
if (this.promptOwners.has(sessionId))
|
|
1806
|
+
busyReasons.push("prompt in flight");
|
|
1698
1807
|
if (session.isStreaming === true || session.isIdle === false)
|
|
1699
1808
|
busyReasons.push("streaming");
|
|
1700
1809
|
if (session.isCompacting === true)
|
|
@@ -1726,6 +1835,7 @@ export class ConsolePiRuntime {
|
|
|
1726
1835
|
const services = this.serviceScopes.get(sessionId);
|
|
1727
1836
|
if (services)
|
|
1728
1837
|
this.applySessionCompactionOverride(sessionId, services, session);
|
|
1838
|
+
await this.refreshModelConfiguration(sessionId, "force");
|
|
1729
1839
|
}
|
|
1730
1840
|
catch (error) {
|
|
1731
1841
|
return {
|
|
@@ -1737,7 +1847,6 @@ export class ConsolePiRuntime {
|
|
|
1737
1847
|
finally {
|
|
1738
1848
|
this.reloadInFlight.delete(sessionId);
|
|
1739
1849
|
}
|
|
1740
|
-
this.bumpRevision(sessionId);
|
|
1741
1850
|
// Reload replaces the effective resource/session context. Persisted Goal
|
|
1742
1851
|
// remains visible, but continuation authorization must be renewed by user.
|
|
1743
1852
|
this.options.goalRegistry?.disarm(sessionId);
|
|
@@ -2256,6 +2365,7 @@ export class ConsolePiRuntime {
|
|
|
2256
2365
|
this.sessions.set(sessionId, session);
|
|
2257
2366
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
2258
2367
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
2368
|
+
this.modelConfigFiles.set(sessionId, built.modelConfigFile);
|
|
2259
2369
|
this.sessionModels.set(sessionId, init.model);
|
|
2260
2370
|
this.serviceScopes.set(sessionId, built.services);
|
|
2261
2371
|
this.revisions.set(sessionId, 1);
|
|
@@ -2309,6 +2419,7 @@ export class ConsolePiRuntime {
|
|
|
2309
2419
|
*/
|
|
2310
2420
|
async buildSessionWithServices(init) {
|
|
2311
2421
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
2422
|
+
const modelConfigFile = await captureModelConfigFile(agentDir);
|
|
2312
2423
|
const appendSystemPrompt = [
|
|
2313
2424
|
OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
2314
2425
|
...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
|
|
@@ -2342,6 +2453,7 @@ export class ConsolePiRuntime {
|
|
|
2342
2453
|
return {
|
|
2343
2454
|
session,
|
|
2344
2455
|
modelRuntime,
|
|
2456
|
+
modelConfigFile,
|
|
2345
2457
|
services: {
|
|
2346
2458
|
resourceLoader: services.resourceLoader,
|
|
2347
2459
|
settingsManager: services.settingsManager,
|
|
@@ -2353,14 +2465,31 @@ export class ConsolePiRuntime {
|
|
|
2353
2465
|
return this.sessions.get(sessionId);
|
|
2354
2466
|
}
|
|
2355
2467
|
/** Inject one queued message into the currently running Pi turn. */
|
|
2356
|
-
async steer(sessionId, text, images) {
|
|
2468
|
+
async steer(sessionId, clientRequestId, text, images) {
|
|
2357
2469
|
const session = this.sessions.get(sessionId);
|
|
2358
2470
|
if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
|
|
2359
2471
|
throw new Error("CHAT_TURN_NOT_ACTIVE");
|
|
2360
2472
|
}
|
|
2361
2473
|
if (typeof session.steer !== "function")
|
|
2362
2474
|
throw new Error("CHAT_STEER_UNAVAILABLE");
|
|
2363
|
-
|
|
2475
|
+
const pending = this.pendingSteering.get(sessionId) ?? [];
|
|
2476
|
+
const identity = { clientRequestId, text };
|
|
2477
|
+
pending.push(identity);
|
|
2478
|
+
this.pendingSteering.set(sessionId, pending);
|
|
2479
|
+
try {
|
|
2480
|
+
await session.steer(text, images);
|
|
2481
|
+
}
|
|
2482
|
+
catch (error) {
|
|
2483
|
+
const current = this.pendingSteering.get(sessionId);
|
|
2484
|
+
if (current) {
|
|
2485
|
+
const index = current.indexOf(identity);
|
|
2486
|
+
if (index >= 0)
|
|
2487
|
+
current.splice(index, 1);
|
|
2488
|
+
if (current.length === 0)
|
|
2489
|
+
this.pendingSteering.delete(sessionId);
|
|
2490
|
+
}
|
|
2491
|
+
throw error;
|
|
2492
|
+
}
|
|
2364
2493
|
}
|
|
2365
2494
|
hasSession(sessionId) {
|
|
2366
2495
|
return this.sessions.has(sessionId);
|
|
@@ -2396,12 +2525,13 @@ export class ConsolePiRuntime {
|
|
|
2396
2525
|
*/
|
|
2397
2526
|
async prompt(sessionId, text, onEvent, options) {
|
|
2398
2527
|
// Synchronous admission fence — before any await.
|
|
2399
|
-
if (this.promptOwners.has(sessionId)) {
|
|
2528
|
+
if (this.promptOwners.has(sessionId) || this.reloadInFlight.has(sessionId)) {
|
|
2400
2529
|
throw new Error("CHAT_TURN_ACTIVE");
|
|
2401
2530
|
}
|
|
2402
2531
|
this.promptOwners.add(sessionId);
|
|
2403
2532
|
let unsub;
|
|
2404
2533
|
let abortPromise;
|
|
2534
|
+
let initialUserMessageStarted = false;
|
|
2405
2535
|
const onAbort = () => {
|
|
2406
2536
|
// M1 T08 / AC-R3-003: Stop must abort only the current turn, NEVER
|
|
2407
2537
|
// dispose the session. Await session.abort() so ownership settles
|
|
@@ -2458,6 +2588,11 @@ export class ConsolePiRuntime {
|
|
|
2458
2588
|
if (options?.signal?.aborted) {
|
|
2459
2589
|
return;
|
|
2460
2590
|
}
|
|
2591
|
+
await this.refreshModelConfiguration(sessionId, "if-changed");
|
|
2592
|
+
// Stop may arrive while stat/refresh/rebind is in flight.
|
|
2593
|
+
if (options?.signal?.aborted) {
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2461
2596
|
const emitContextUsage = () => {
|
|
2462
2597
|
try {
|
|
2463
2598
|
const contextUsage = session.getSessionStats?.()?.contextUsage;
|
|
@@ -2499,6 +2634,27 @@ export class ConsolePiRuntime {
|
|
|
2499
2634
|
};
|
|
2500
2635
|
unsub = session.subscribe((event) => {
|
|
2501
2636
|
const observedAt = Date.now();
|
|
2637
|
+
if (event.type === "message_start" && isUserSdkMessage(event.message)) {
|
|
2638
|
+
if (!initialUserMessageStarted) {
|
|
2639
|
+
// The first user message_start belongs to session.prompt(text), not
|
|
2640
|
+
// to a steering request that may have raced with prompt startup.
|
|
2641
|
+
initialUserMessageStarted = true;
|
|
2642
|
+
}
|
|
2643
|
+
else {
|
|
2644
|
+
const pending = this.pendingSteering.get(sessionId);
|
|
2645
|
+
const accepted = pending?.shift();
|
|
2646
|
+
if (pending?.length === 0)
|
|
2647
|
+
this.pendingSteering.delete(sessionId);
|
|
2648
|
+
if (accepted) {
|
|
2649
|
+
onEvent({
|
|
2650
|
+
type: "steering_accepted",
|
|
2651
|
+
sessionId,
|
|
2652
|
+
clientRequestId: accepted.clientRequestId,
|
|
2653
|
+
text: accepted.text,
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2502
2658
|
if (event.type === "compaction_start") {
|
|
2503
2659
|
activeCompaction = {
|
|
2504
2660
|
compactionId: newCompactionId(sessionId),
|
|
@@ -2637,6 +2793,10 @@ export class ConsolePiRuntime {
|
|
|
2637
2793
|
finally {
|
|
2638
2794
|
if (this.options.questionRegistry)
|
|
2639
2795
|
this.options.questionRegistry.clearTurn(sessionId);
|
|
2796
|
+
// Unconsumed durable DeferredTurns remain dispatching and are recovered by
|
|
2797
|
+
// the route layer after this active Turn closes. Never carry their volatile
|
|
2798
|
+
// identities into a later prompt.
|
|
2799
|
+
this.pendingSteering.delete(sessionId);
|
|
2640
2800
|
this.promptOwners.delete(sessionId);
|
|
2641
2801
|
}
|
|
2642
2802
|
}
|
|
@@ -2758,11 +2918,13 @@ export class ConsolePiRuntime {
|
|
|
2758
2918
|
this.sessionManagers.delete(sessionId);
|
|
2759
2919
|
this.mainlineLeaves.delete(sessionId);
|
|
2760
2920
|
this.modelRuntimes.delete(sessionId);
|
|
2921
|
+
this.modelConfigFiles.delete(sessionId);
|
|
2761
2922
|
this.sessionModels.delete(sessionId);
|
|
2762
2923
|
this.serviceScopes.delete(sessionId);
|
|
2763
2924
|
this.revisions.delete(sessionId);
|
|
2764
2925
|
this.thinkingSelections.delete(sessionId);
|
|
2765
2926
|
this.reloadInFlight.delete(sessionId);
|
|
2927
|
+
this.pendingSteering.delete(sessionId);
|
|
2766
2928
|
}
|
|
2767
2929
|
disposeAll() {
|
|
2768
2930
|
for (const id of [...this.sessions.keys()])
|
|
@@ -2795,6 +2957,11 @@ function mapAssistantMessageEvent(type, sessionId, message) {
|
|
|
2795
2957
|
...(content.length > 0 ? { content } : {}),
|
|
2796
2958
|
};
|
|
2797
2959
|
}
|
|
2960
|
+
function isUserSdkMessage(message) {
|
|
2961
|
+
return Boolean(message &&
|
|
2962
|
+
typeof message === "object" &&
|
|
2963
|
+
message.role === "user");
|
|
2964
|
+
}
|
|
2798
2965
|
function mapSdkEvent(sessionId, event, compactionId) {
|
|
2799
2966
|
switch (event.type) {
|
|
2800
2967
|
case "agent_start":
|
|
@@ -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
|
}
|