@tea-agent/loop-agent 0.42.0-next.2 → 0.42.0-next.3

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +242 -1
  2. package/dist/application/dag/generate-task-dag.js +75 -19
  3. package/dist/application/task-lifecycle/advance.js +24 -5
  4. package/dist/application/task-lifecycle/observe.js +171 -17
  5. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +7 -0
  9. package/dist/cli/program.js +5 -0
  10. package/dist/commands/client-recovery.js +3 -0
  11. package/dist/commands/dag-follow-up.js +138 -0
  12. package/dist/commands/init.js +27 -1
  13. package/dist/commands/task-advance.js +19 -0
  14. package/dist/executors/dag-pi-executor.js +3784 -231
  15. package/dist/executors/pi-executor.js +15 -4
  16. package/dist/executors/pi-read-budget-policy.js +239 -0
  17. package/dist/executors/shell-executor.js +942 -187
  18. package/dist/executors/shell-write-guard.js +7 -0
  19. package/dist/infrastructure/console/operation-store.js +226 -6
  20. package/dist/shared/dag-failure-category.js +12 -0
  21. package/dist/shared/openspec-spec.js +70 -4
  22. package/dist/shared/operator/capabilities.js +7 -0
  23. package/dist/task/config-types.js +34 -5
  24. package/dist/task/contract/adopt.js +4 -0
  25. package/dist/task/contract/import-revision.js +4 -0
  26. package/dist/task/contract/project.js +3 -0
  27. package/dist/task/contract/schema.js +2 -1
  28. package/dist/task/frontend-project-capability.js +203 -20
  29. package/dist/task/runtime.js +5 -2
  30. package/dist/task/source-prepare/build-draft.js +3 -3
  31. package/dist/task/source-prepare/fragment-inventory.js +15 -9
  32. package/dist/task/source-prepare/prepare.js +89 -1
  33. package/dist/task/source-prepare/semantic-intake.js +6 -2
  34. package/dist/task/source-references.js +22 -1
  35. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  36. package/dist/worker/console/chat/pi-runtime.js +173 -7
  37. package/dist/worker/console/chat/repo-browser.js +10 -3
  38. package/dist/worker/console/chat/routes.js +30 -19
  39. package/dist/worker/console/chat/session-store.js +117 -70
  40. package/dist/worker/console/chat/shortcuts.js +1 -1
  41. package/dist/worker/console/chat/turn-process.js +111 -56
  42. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  43. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  44. package/dist/worker/console/index.js +3 -0
  45. package/dist/worker/console/operator-actions.js +169 -7
  46. package/dist/worker/console/server.js +3 -0
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
  79. package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
  80. package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  107. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  108. package/dist/worker/console/workspace-context.js +111 -1
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  111. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  112. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  113. package/dist/workflows/dag/frontend-closeout.js +221 -0
  114. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  115. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  116. package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
  117. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
  119. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  120. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  121. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  122. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  123. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  124. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  125. package/dist/workflows/dag/frontend-repair.js +1 -432
  126. package/dist/workflows/dag/frontend-review-context.js +261 -15
  127. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  128. package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
  129. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  130. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  131. package/dist/workflows/dag/frontend-shape.js +435 -0
  132. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  133. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  134. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  135. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  136. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  137. package/dist/workflows/dag/frontend-verification-trace.js +249 -24
  138. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  139. package/dist/workflows/dag/frontend-writer-admission.js +285 -0
  140. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  141. package/dist/workflows/dag/init-hybrid.js +677 -492
  142. package/dist/workflows/dag/interrupt-request.js +7 -0
  143. package/dist/workflows/dag/node-execution.js +732 -4
  144. package/dist/workflows/dag/prompt.js +34 -1
  145. package/dist/workflows/dag/recovery-lease.js +80 -0
  146. package/dist/workflows/dag/report.js +37 -1
  147. package/dist/workflows/dag/rerun-feedback.js +256 -1
  148. package/dist/workflows/dag/rerun-plan.js +17 -6
  149. package/dist/workflows/dag/rerun-run.js +23 -0
  150. package/dist/workflows/dag/rerun-task.js +29 -0
  151. package/dist/workflows/dag/retry-policy.js +214 -104
  152. package/dist/workflows/dag/runner.js +433 -126
  153. package/dist/workflows/dag/scheduler.js +114 -16
  154. package/dist/workflows/dag/types.js +239 -12
  155. package/dist/workflows/dag/validate.js +28 -12
  156. package/docs/examples/README.md +5 -0
  157. package/docs/init-surface.manifest.json +30 -12
  158. package/docs/skills/vetted-skill-registry.md +4 -2
  159. package/docs/templates/README.md +2 -0
  160. package/docs/templates/agent-dag-report.schema.json +8 -2
  161. package/docs/templates/agent-dag.schema.json +1 -1
  162. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  163. package/docs/templates/frontend-implementation-dag.json +89 -0
  164. package/docs/templates/spec-registry.schema.json +45 -0
  165. package/harness.json +1 -1
  166. package/package.json +1 -1
  167. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  168. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  169. package/skills/frontend-contract/SKILL.md +23 -0
  170. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  171. package/skills/frontend-design-review/SKILL.md +22 -41
  172. package/skills/frontend-plan/SKILL.md +26 -0
  173. package/skills/frontend-plan/references/decision-contract.md +37 -0
  174. package/skills/frontend-plan/references/design-decisions.md +17 -0
  175. package/skills/frontend-review/SKILL.md +20 -15
  176. package/skills/frontend-review/references/review-findings.md +6 -7
  177. package/skills/frontend-scout/SKILL.md +25 -0
  178. package/skills/frontend-scout/references/design-evidence.md +16 -0
  179. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  180. package/skills/frontend-verification/SKILL.md +1 -1
  181. package/skills/loop-agent/references/command-reference.md +1 -0
  182. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  183. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  185. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  186. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  187. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  188. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  189. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  190. package/skills/frontend-implementation/SKILL.md +0 -52
  191. package/skills/frontend-implementation/references/code-standards.md +0 -33
  192. package/skills/frontend-implementation/references/design-spec.md +0 -56
  193. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -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";
@@ -931,6 +931,44 @@ export class PiSessionInitFailedError extends Error {
931
931
  this.name = "PiSessionInitFailedError";
932
932
  }
933
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
+ }
934
972
  /**
935
973
  * Console Pi runtime — holds Chat sessions and drives prompt() with three-gate
936
974
  * tool enforcement. Session-per-sessionId; cross-request reuse via open().
@@ -940,6 +978,8 @@ export class ConsolePiRuntime {
940
978
  loader;
941
979
  sessions = new Map();
942
980
  modelRuntimes = new Map();
981
+ /** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
982
+ modelConfigFiles = new Map();
943
983
  sessionManagers = new Map();
944
984
  /** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
945
985
  serviceScopes = new Map();
@@ -949,6 +989,8 @@ export class ConsolePiRuntime {
949
989
  reloadInFlight = new Set();
950
990
  /** Synchronous prompt ownership per session (admission before first await). */
951
991
  promptOwners = new Set();
992
+ /** Steering requests accepted by the SDK but not yet consumed into Pi context. */
993
+ pendingSteering = new Map();
952
994
  /** At most one detached automatic-title task per durable Console session. */
953
995
  titleInFlight = new Set();
954
996
  /** Composer thinking selection ("auto" = no explicit override). */
@@ -1341,6 +1383,9 @@ export class ConsolePiRuntime {
1341
1383
  if (this.disposedSessions.has(sessionId))
1342
1384
  return;
1343
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);
1344
1389
  const appendSystemPrompt = [
1345
1390
  OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1346
1391
  ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
@@ -1400,6 +1445,7 @@ export class ConsolePiRuntime {
1400
1445
  this.sessions.set(sessionId, session);
1401
1446
  this.sessionManagers.set(sessionId, init.sessionManager);
1402
1447
  this.modelRuntimes.set(sessionId, modelRuntime);
1448
+ this.modelConfigFiles.set(sessionId, modelConfigFile);
1403
1449
  this.sessionModels.set(sessionId, model);
1404
1450
  this.serviceScopes.set(sessionId, {
1405
1451
  resourceLoader: services.resourceLoader,
@@ -1420,6 +1466,7 @@ export class ConsolePiRuntime {
1420
1466
  this.sessions.delete(sessionId);
1421
1467
  this.sessionManagers.delete(sessionId);
1422
1468
  this.modelRuntimes.delete(sessionId);
1469
+ this.modelConfigFiles.delete(sessionId);
1423
1470
  this.sessionModels.delete(sessionId);
1424
1471
  this.serviceScopes.delete(sessionId);
1425
1472
  this.revisions.delete(sessionId);
@@ -1498,6 +1545,7 @@ export class ConsolePiRuntime {
1498
1545
  this.sessions.set(sessionId, built.session);
1499
1546
  this.sessionManagers.set(sessionId, sessionManager);
1500
1547
  this.modelRuntimes.set(sessionId, built.modelRuntime);
1548
+ this.modelConfigFiles.set(sessionId, built.modelConfigFile);
1501
1549
  this.sessionModels.set(sessionId, init.model);
1502
1550
  this.serviceScopes.set(sessionId, built.services);
1503
1551
  this.revisions.set(sessionId, 1);
@@ -1569,11 +1617,69 @@ export class ConsolePiRuntime {
1569
1617
  if (!resolved)
1570
1618
  throw new Error("MODEL_NOT_AVAILABLE");
1571
1619
  await session.setModel(resolved);
1572
- this.bumpRevision(sessionId);
1573
- return {
1620
+ const applied = {
1574
1621
  provider: session.model?.provider ?? model.provider,
1575
1622
  modelId: session.model?.id ?? session.model?.modelId ?? model.modelId,
1576
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;
1577
1683
  }
1578
1684
  /**
1579
1685
  * Apply a thinking-level selection. `auto` records the selection WITHOUT
@@ -1696,6 +1802,8 @@ export class ConsolePiRuntime {
1696
1802
  };
1697
1803
  }
1698
1804
  const busyReasons = [];
1805
+ if (this.promptOwners.has(sessionId))
1806
+ busyReasons.push("prompt in flight");
1699
1807
  if (session.isStreaming === true || session.isIdle === false)
1700
1808
  busyReasons.push("streaming");
1701
1809
  if (session.isCompacting === true)
@@ -1727,6 +1835,7 @@ export class ConsolePiRuntime {
1727
1835
  const services = this.serviceScopes.get(sessionId);
1728
1836
  if (services)
1729
1837
  this.applySessionCompactionOverride(sessionId, services, session);
1838
+ await this.refreshModelConfiguration(sessionId, "force");
1730
1839
  }
1731
1840
  catch (error) {
1732
1841
  return {
@@ -1738,7 +1847,6 @@ export class ConsolePiRuntime {
1738
1847
  finally {
1739
1848
  this.reloadInFlight.delete(sessionId);
1740
1849
  }
1741
- this.bumpRevision(sessionId);
1742
1850
  // Reload replaces the effective resource/session context. Persisted Goal
1743
1851
  // remains visible, but continuation authorization must be renewed by user.
1744
1852
  this.options.goalRegistry?.disarm(sessionId);
@@ -2257,6 +2365,7 @@ export class ConsolePiRuntime {
2257
2365
  this.sessions.set(sessionId, session);
2258
2366
  this.sessionManagers.set(sessionId, sessionManager);
2259
2367
  this.modelRuntimes.set(sessionId, built.modelRuntime);
2368
+ this.modelConfigFiles.set(sessionId, built.modelConfigFile);
2260
2369
  this.sessionModels.set(sessionId, init.model);
2261
2370
  this.serviceScopes.set(sessionId, built.services);
2262
2371
  this.revisions.set(sessionId, 1);
@@ -2310,6 +2419,7 @@ export class ConsolePiRuntime {
2310
2419
  */
2311
2420
  async buildSessionWithServices(init) {
2312
2421
  const agentDir = await safeGetAgentDir(this.bindings);
2422
+ const modelConfigFile = await captureModelConfigFile(agentDir);
2313
2423
  const appendSystemPrompt = [
2314
2424
  OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
2315
2425
  ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
@@ -2343,6 +2453,7 @@ export class ConsolePiRuntime {
2343
2453
  return {
2344
2454
  session,
2345
2455
  modelRuntime,
2456
+ modelConfigFile,
2346
2457
  services: {
2347
2458
  resourceLoader: services.resourceLoader,
2348
2459
  settingsManager: services.settingsManager,
@@ -2354,14 +2465,31 @@ export class ConsolePiRuntime {
2354
2465
  return this.sessions.get(sessionId);
2355
2466
  }
2356
2467
  /** Inject one queued message into the currently running Pi turn. */
2357
- async steer(sessionId, text, images) {
2468
+ async steer(sessionId, clientRequestId, text, images) {
2358
2469
  const session = this.sessions.get(sessionId);
2359
2470
  if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
2360
2471
  throw new Error("CHAT_TURN_NOT_ACTIVE");
2361
2472
  }
2362
2473
  if (typeof session.steer !== "function")
2363
2474
  throw new Error("CHAT_STEER_UNAVAILABLE");
2364
- await session.steer(text, images);
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
+ }
2365
2493
  }
2366
2494
  hasSession(sessionId) {
2367
2495
  return this.sessions.has(sessionId);
@@ -2397,12 +2525,13 @@ export class ConsolePiRuntime {
2397
2525
  */
2398
2526
  async prompt(sessionId, text, onEvent, options) {
2399
2527
  // Synchronous admission fence — before any await.
2400
- if (this.promptOwners.has(sessionId)) {
2528
+ if (this.promptOwners.has(sessionId) || this.reloadInFlight.has(sessionId)) {
2401
2529
  throw new Error("CHAT_TURN_ACTIVE");
2402
2530
  }
2403
2531
  this.promptOwners.add(sessionId);
2404
2532
  let unsub;
2405
2533
  let abortPromise;
2534
+ let initialUserMessageStarted = false;
2406
2535
  const onAbort = () => {
2407
2536
  // M1 T08 / AC-R3-003: Stop must abort only the current turn, NEVER
2408
2537
  // dispose the session. Await session.abort() so ownership settles
@@ -2459,6 +2588,11 @@ export class ConsolePiRuntime {
2459
2588
  if (options?.signal?.aborted) {
2460
2589
  return;
2461
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
+ }
2462
2596
  const emitContextUsage = () => {
2463
2597
  try {
2464
2598
  const contextUsage = session.getSessionStats?.()?.contextUsage;
@@ -2500,6 +2634,27 @@ export class ConsolePiRuntime {
2500
2634
  };
2501
2635
  unsub = session.subscribe((event) => {
2502
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
+ }
2503
2658
  if (event.type === "compaction_start") {
2504
2659
  activeCompaction = {
2505
2660
  compactionId: newCompactionId(sessionId),
@@ -2638,6 +2793,10 @@ export class ConsolePiRuntime {
2638
2793
  finally {
2639
2794
  if (this.options.questionRegistry)
2640
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);
2641
2800
  this.promptOwners.delete(sessionId);
2642
2801
  }
2643
2802
  }
@@ -2759,11 +2918,13 @@ export class ConsolePiRuntime {
2759
2918
  this.sessionManagers.delete(sessionId);
2760
2919
  this.mainlineLeaves.delete(sessionId);
2761
2920
  this.modelRuntimes.delete(sessionId);
2921
+ this.modelConfigFiles.delete(sessionId);
2762
2922
  this.sessionModels.delete(sessionId);
2763
2923
  this.serviceScopes.delete(sessionId);
2764
2924
  this.revisions.delete(sessionId);
2765
2925
  this.thinkingSelections.delete(sessionId);
2766
2926
  this.reloadInFlight.delete(sessionId);
2927
+ this.pendingSteering.delete(sessionId);
2767
2928
  }
2768
2929
  disposeAll() {
2769
2930
  for (const id of [...this.sessions.keys()])
@@ -2796,6 +2957,11 @@ function mapAssistantMessageEvent(type, sessionId, message) {
2796
2957
  ...(content.length > 0 ? { content } : {}),
2797
2958
  };
2798
2959
  }
2960
+ function isUserSdkMessage(message) {
2961
+ return Boolean(message &&
2962
+ typeof message === "object" &&
2963
+ message.role === "user");
2964
+ }
2799
2965
  function mapSdkEvent(sessionId, event, compactionId) {
2800
2966
  switch (event.type) {
2801
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
- if (info.size > HTML_PREVIEW_ASSET_LIMIT)
194
- throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${HTML_PREVIEW_ASSET_LIMIT} byte HTML preview limit`);
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: repoHtmlAssetContentType(guard.repoRelative),
212
+ contentType,
206
213
  bytes: info.size,
207
214
  };
208
215
  }
@@ -1065,20 +1065,10 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1065
1065
  }
1066
1066
  const item = claim.deferredTurn;
1067
1067
  try {
1068
- await deps.runtime.steer(sessionId, item.text, item.images);
1069
- const userMessage = await deps.store.appendMessage(sessionId, {
1070
- role: "user",
1071
- text: item.text,
1072
- });
1073
- deps.events.append(sessionId, active.turnId, {
1074
- kind: "user_message",
1075
- data: {
1076
- clientRequestId: item.clientRequestId,
1077
- message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
1078
- },
1079
- });
1080
- await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
1081
- await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
1068
+ await deps.runtime.steer(sessionId, item.clientRequestId, item.text, item.images);
1069
+ // session.steer() only acknowledges queueing. Keep the durable item in
1070
+ // dispatching until Pi emits the corresponding user message_start.
1071
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, item, item.revision);
1082
1072
  sendJson(res, 200, {
1083
1073
  ok: true,
1084
1074
  accepted: true,
@@ -1102,6 +1092,21 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1102
1092
  });
1103
1093
  }
1104
1094
  }
1095
+ async function persistAcceptedSteering(deps, sessionId, turnId, userMessage, clientRequestId) {
1096
+ deps.events.append(sessionId, turnId, {
1097
+ kind: "user_message",
1098
+ data: {
1099
+ clientRequestId,
1100
+ steering: true,
1101
+ message: {
1102
+ id: userMessage.id,
1103
+ text: userMessage.text,
1104
+ at: userMessage.at,
1105
+ },
1106
+ },
1107
+ });
1108
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, 0);
1109
+ }
1105
1110
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
1106
1111
  * get_state equivalent): exposes the active turn + the latest turn's durable
1107
1112
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -1661,6 +1666,9 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1661
1666
  if (!res.writableEnded)
1662
1667
  writeChatSse(res, persisted);
1663
1668
  },
1669
+ onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
1670
+ await persistAcceptedSteering(deps, sessionId, turnId, userMessage, accepted.clientRequestId);
1671
+ },
1664
1672
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1665
1673
  sessionId,
1666
1674
  turnId,
@@ -1971,6 +1979,9 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1971
1979
  if (placeholder)
1972
1980
  emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
1973
1981
  },
1982
+ onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
1983
+ await persistAcceptedSteering(deps, sessionId, turn.turnId, userMessage, accepted.clientRequestId);
1984
+ },
1974
1985
  persistUserMessage: trustedPayload?.persistUserMessage,
1975
1986
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1976
1987
  sessionId,
@@ -2646,13 +2657,13 @@ async function handleRepoHtmlPreview(res, deps, rawPath) {
2646
2657
  "Cache-Control": "no-store",
2647
2658
  "X-Content-Type-Options": "nosniff",
2648
2659
  "Referrer-Policy": "no-referrer",
2649
- // The iframe also has an empty sandbox attribute. This response-level
2650
- // sandbox keeps direct/popup loads isolated, while explicit local-origin
2651
- // style/image/font/media sources let relative project assets render.
2660
+ // Project scripts may drive tabs and other local interactions, but the
2661
+ // opaque sandbox still withholds same-origin access, forms, popups and
2662
+ // top-level navigation. Network requests remain disabled below.
2652
2663
  "Content-Security-Policy": [
2653
- "sandbox",
2664
+ "sandbox allow-scripts",
2654
2665
  "default-src 'none'",
2655
- "script-src 'none'",
2666
+ `script-src 'unsafe-inline' ${origin}`,
2656
2667
  "object-src 'none'",
2657
2668
  "frame-src 'none'",
2658
2669
  "form-action 'none'",