@piagent/platform 1.5.5 → 1.6.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.
Files changed (323) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +25 -21
  3. package/README.vi.md +34 -5
  4. package/adapters/web-frontend/profile.json +2 -4
  5. package/architecture/layers.json +18 -3
  6. package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
  7. package/benchmarks/deep-logic-v1/grade.mjs +272 -10
  8. package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
  9. package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
  10. package/benchmarks/deep-logic-v1/project/package.json +1 -1
  11. package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
  12. package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
  13. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
  14. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
  15. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
  16. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
  17. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
  18. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
  19. package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
  20. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
  21. package/benchmarks/deep-logic-v1/suite.json +51 -3
  22. package/benchmarks/deep-logic-v1/variant.mjs +22 -1
  23. package/benchmarks/production-v1/spend-control.v1.json +92 -0
  24. package/benchmarks/production-v1/suite.json +46 -3
  25. package/catalog/capabilities.json +18 -18
  26. package/docs/authority-manifest.md +1 -1
  27. package/docs/auto-delegation-policy.md +38 -73
  28. package/docs/command-reference-vietnamese.md +27 -35
  29. package/docs/context-engine.md +12 -4
  30. package/docs/context-window-policy.md +37 -1
  31. package/docs/distribution-standard.md +7 -7
  32. package/docs/en/architecture.md +27 -13
  33. package/docs/languages.json +4 -4
  34. package/docs/model-options.md +8 -5
  35. package/docs/operator-manual-vietnamese.md +27 -45
  36. package/docs/project-adapters.md +2 -2
  37. package/docs/publishing-for-teams.md +10 -0
  38. package/docs/quality-benchmark.md +427 -62
  39. package/docs/release-install-policy.md +10 -9
  40. package/docs/runtime-harness-standard.md +3 -3
  41. package/docs/runtime-policy-design.md +4 -4
  42. package/docs/runtime-quality-baseline.md +13 -6
  43. package/docs/security-threat-model.md +2 -2
  44. package/docs/subagent-orchestration-capabilities.md +19 -51
  45. package/docs/subagents-and-multiagent.md +21 -47
  46. package/docs/task-implementation-contract.md +3 -3
  47. package/docs/task-lifecycle-vietnamese.md +3 -3
  48. package/docs/team-onboarding.md +2 -2
  49. package/docs/usage-observability.md +7 -2
  50. package/docs/vi/architecture.md +27 -13
  51. package/docs-site/architecture.html +1 -1
  52. package/docs-site/benchmark.html +89 -95
  53. package/docs-site/commands.html +1 -1
  54. package/docs-site/content/benchmark.html +88 -94
  55. package/docs-site/content/en/benchmark.html +134 -90
  56. package/docs-site/content/en/index.html +19 -19
  57. package/docs-site/content/en/runtime.html +4 -4
  58. package/docs-site/content/en/whats-new.html +12 -1
  59. package/docs-site/content/en/workflows.html +2 -2
  60. package/docs-site/content/index.html +19 -19
  61. package/docs-site/content/runtime.html +4 -4
  62. package/docs-site/content/whats-new.html +12 -1
  63. package/docs-site/content/workflows.html +2 -2
  64. package/docs-site/context-engine.html +1 -1
  65. package/docs-site/en/architecture.html +1 -1
  66. package/docs-site/en/benchmark.html +135 -91
  67. package/docs-site/en/commands.html +1 -1
  68. package/docs-site/en/context-engine.html +1 -1
  69. package/docs-site/en/index.html +20 -20
  70. package/docs-site/en/mcp-approval.html +1 -1
  71. package/docs-site/en/mcp-auth.html +1 -1
  72. package/docs-site/en/mcp-servers.html +1 -1
  73. package/docs-site/en/mcp.html +1 -1
  74. package/docs-site/en/permissions.html +1 -1
  75. package/docs-site/en/profiles.html +1 -1
  76. package/docs-site/en/quickstart.html +1 -1
  77. package/docs-site/en/runtime.html +5 -5
  78. package/docs-site/en/scope.html +1 -1
  79. package/docs-site/en/security.html +1 -1
  80. package/docs-site/en/team.html +1 -1
  81. package/docs-site/en/whats-new.html +13 -2
  82. package/docs-site/en/workflows.html +3 -3
  83. package/docs-site/index.html +20 -20
  84. package/docs-site/mcp-approval.html +1 -1
  85. package/docs-site/mcp-auth.html +1 -1
  86. package/docs-site/mcp-servers.html +1 -1
  87. package/docs-site/mcp.html +1 -1
  88. package/docs-site/permissions.html +1 -1
  89. package/docs-site/profiles.html +1 -1
  90. package/docs-site/quickstart.html +1 -1
  91. package/docs-site/runtime.html +5 -5
  92. package/docs-site/scope.html +1 -1
  93. package/docs-site/security.html +1 -1
  94. package/docs-site/team.html +1 -1
  95. package/docs-site/whats-new.html +13 -2
  96. package/docs-site/workflows.html +3 -3
  97. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  98. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  99. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  100. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  101. package/evals/fixtures/helper-request.invalid.json +1 -1
  102. package/evals/fixtures/helper-request.valid.json +1 -1
  103. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  104. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  105. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  106. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  107. package/evals/fs4-readiness-matrix.v1.json +2 -2
  108. package/evals/fs5-causal-arm.v1.json +3 -3
  109. package/evals/fs5-pilot-protocol.v1.json +3 -3
  110. package/evals/fs5-pilot-protocol.v2.json +8 -8
  111. package/evals/fs5-pilot-protocol.v3.json +8 -8
  112. package/evals/fs5-pilot-protocol.v4.json +7 -6
  113. package/evals/fs5-pilot-protocol.v5.json +9 -7
  114. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  115. package/evals/runtime-conformance-v1/README.md +43 -0
  116. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  117. package/evals/runtime-conformance-v1/lane.json +57 -0
  118. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  119. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  120. package/package.json +3 -1
  121. package/packages/piagent-core/README.md +13 -4
  122. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  123. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  124. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  125. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  126. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  127. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  128. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  129. package/packages/piagent-core/benchmark/benchmark-core.js +736 -664
  130. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  131. package/packages/piagent-core/benchmark/benchmark-forensics.js +441 -4
  132. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  133. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  134. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  135. package/packages/piagent-core/benchmark/benchmark-preflight.js +5 -1
  136. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  137. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  138. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  139. package/packages/piagent-core/benchmark/benchmark-record-validation.js +252 -0
  140. package/packages/piagent-core/benchmark/benchmark-report.js +130 -12
  141. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  142. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  143. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  144. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +979 -0
  145. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  146. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  147. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  148. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  149. package/packages/piagent-core/benchmark/benchmark-suite.js +149 -2
  150. package/packages/piagent-core/benchmark/benchmark-summary-support.js +380 -0
  151. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  152. package/packages/piagent-core/benchmark/benchmark-usage.js +322 -0
  153. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  154. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  155. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  156. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  157. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  158. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  159. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  160. package/packages/piagent-core/extensions/acceptance-receipt.js +123 -59
  161. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  162. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  163. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  164. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  165. package/packages/piagent-core/extensions/context-engine.js +149 -156
  166. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  167. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  168. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  169. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  170. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  171. package/packages/piagent-core/extensions/criterion-context-pack.js +435 -7
  172. package/packages/piagent-core/extensions/criterion-graph.js +82 -9
  173. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  174. package/packages/piagent-core/extensions/guard-types.ts +6 -0
  175. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  176. package/packages/piagent-core/extensions/piagent-guard.ts +420 -260
  177. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  178. package/packages/piagent-core/extensions/task-lifecycle.js +39 -2
  179. package/packages/piagent-core/extensions/task-state.js +109 -142
  180. package/packages/piagent-core/extensions/verification-intelligence.js +52 -13
  181. package/packages/piagent-core/extensions/workspace-evidence-roots.js +193 -0
  182. package/packages/piagent-core/package.json +1 -1
  183. package/packages/piagent-core/policies/base-policy.json +44 -2
  184. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  185. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  186. package/packages/piagent-core/prompts/plan.md +1 -1
  187. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  188. package/packages/piagent-core/prompts/review.md +2 -7
  189. package/packages/piagent-core/prompts/scout.md +1 -1
  190. package/packages/piagent-core/prompts/task.md +4 -3
  191. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  192. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  193. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  194. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  195. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  196. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  197. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  198. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  199. package/packages/piagent-core/runtime/hooks/completion-hook.ts +12 -12
  200. package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -1
  201. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  202. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +107 -3
  203. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  204. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +134 -166
  205. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  206. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  207. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  208. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  209. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  210. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  211. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  212. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  213. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +70 -0
  214. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +73 -9
  215. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  216. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  217. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  218. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  219. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  220. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  221. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  222. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  223. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  224. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  225. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +7 -2
  226. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  227. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +22 -11
  228. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  229. package/packages/piagent-core/runtime/quality/performance-assurance.ts +9 -5
  230. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  231. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  232. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  233. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  234. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  235. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  236. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  237. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +60 -13
  238. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  239. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  240. package/packages/piagent-core/runtime/registration/context-commands.ts +82 -7
  241. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  242. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  243. package/packages/piagent-core/runtime/registration/policy-tools.ts +50 -15
  244. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  245. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +61 -25
  246. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  247. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +169 -0
  248. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  249. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  250. package/packages/piagent-core/runtime/registration/task-start-tool.ts +108 -103
  251. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  252. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  253. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  254. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  255. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  256. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  257. package/packages/piagent-core/runtime/session/runtime-state.ts +161 -5
  258. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  259. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  260. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  261. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  262. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  263. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  264. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  265. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  266. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  267. package/packages/piagent-core/runtime/workflows/task-intake.ts +207 -38
  268. package/packages/piagent-core/security/sensitive-data.js +13 -218
  269. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  270. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  271. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  272. package/packages/piagent-core/security/sensitive-text.js +234 -0
  273. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  274. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  275. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  276. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  277. package/packages/piagent-core/subagents/piagent-worker.md +7 -5
  278. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-B4XehTCU.js} +1 -1
  279. package/packages/piagent-webui/dist/client/assets/index-CgOEyRPG.js +176 -0
  280. package/packages/piagent-webui/dist/client/index.html +1 -1
  281. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  282. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  283. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  284. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  285. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  286. package/packages/piagent-webui/gateway/gateway-session-stream.ts +122 -20
  287. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  288. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  289. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  290. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  291. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  292. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  293. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +165 -0
  294. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  295. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +141 -125
  296. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  297. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  298. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  299. package/packages/piagent-webui/server/transcript-projection.ts +113 -9
  300. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  301. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  302. package/schemas/piagent-webui/catalog-v1.json +9 -0
  303. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  304. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  305. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  306. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  307. package/schemas/project-profile.schema.json +1 -1
  308. package/schemas/task-contract.schema.json +27 -1
  309. package/scripts/benchmark-runner-core.mjs +367 -363
  310. package/scripts/benchmark-runner-finalization.mjs +313 -0
  311. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  312. package/scripts/benchmark-runner-support.mjs +200 -0
  313. package/scripts/benchmark-session.mjs +95 -41
  314. package/scripts/configure-model-scope.sh +4 -3
  315. package/scripts/configure-subagents.sh +42 -34
  316. package/scripts/helper-evaluation.mjs +2 -2
  317. package/scripts/install-global.sh +2 -2
  318. package/scripts/pi-usage-history.mjs +37 -10
  319. package/scripts/setup.sh +2 -2
  320. package/scripts/team-doctor.sh +1 -1
  321. package/scripts/verify-local.sh +10 -1
  322. package/templates/global/settings.json +2 -1
  323. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -1,8 +1,5 @@
1
1
  import { createHmac, randomBytes } from "node:crypto";
2
- import path from "node:path";
3
-
4
- import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
5
- import { webUiTaskRevision } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
2
+ import { webUiModelRef, webUiTaskRevision } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
6
3
  import { activeSessionTask } from "../../piagent-core/extensions/task-state.js";
7
4
  import { inspectTaskControlState } from "../../piagent-core/runtime/inspection/task-control-journal.ts";
8
5
  import { piApprovalBroker, type ApprovalAuthority, type ApprovalBrokerEvent } from "../../piagent-core/runtime/inspection/approval-broker.ts";
@@ -11,52 +8,23 @@ import { isUserConversationSession, projectRefForCwd, sessionRefForPath } from "
11
8
  import { SessionLeaseStore, type SessionLeaseSnapshot } from "./session-lease-store.ts";
12
9
  import { GatewayEventStore } from "./gateway-events.ts";
13
10
  import { GatewaySessionStream } from "./gateway-session-stream.ts";
14
- import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
15
11
  import { executePermissionCommand, executeRuntimeCommand } from "./runtime-session-controls.ts";
16
- import { rpcUiContext } from "./rpc-ui-context.ts";
12
+ import { configureSessionOptions, effectiveModelRef, effectiveThinkingLevel, type EffectiveSessionOptions } from "./session-effective-options.ts";
17
13
  import { waitForOperationStart } from "./session-operation-start.ts";
18
-
14
+ import { armSessionOperationWatchdog, bestEffortUnsubscribe, boundedResult, sessionOperationDeadlinePolicy, SessionOperationWatchdog, terminateWatchedSessionOperation,
15
+ type SessionOperationDeadlinePolicy, type SessionOperationWatchdogOptions } from "./session-operation-watchdog.ts";
16
+ import { createProductionRuntimeFactory, type RuntimeFactory, type RuntimeHandle } from "./session-runtime-factory.ts";
19
17
  const MAX_WARM_RUNTIMES = 10;
20
-
21
- type RuntimeHandle = { dispose(): Promise<void>; session?: any };
22
- type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string, sessionManager?: any) => Promise<RuntimeHandle>;
23
18
  type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info: PiSessionInfo; operationRef: string | null;
24
- unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
25
- unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null };
19
+ stream: GatewaySessionStream | null; unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
20
+ unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null;
21
+ watchdog: SessionOperationWatchdog | null; lastSessionRevision: string | null };
26
22
  type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
27
23
  type RuntimeCommandResult = Awaited<ReturnType<typeof executeRuntimeCommand>>;
28
-
29
- function productionRuntimeFactory(options: { host: any; agentDir: string; packageRoot: string; modelRuntime?: any }): RuntimeFactory {
30
- return async (info, _runtimeInstanceRef, sessionManager) => {
31
- const guard = path.join(options.packageRoot, "packages", "piagent-core", "extensions", "piagent-guard.ts");
32
- const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
33
- const services = await options.host.createAgentSessionServices({
34
- cwd, agentDir, modelRuntime: options.modelRuntime,
35
- resourceLoaderOptions: {
36
- additionalExtensionPaths: [guard],
37
- extensionsOverride: preferAuthoritativePiagentGuard(guard)
38
- }
39
- });
40
- const extensionErrors = services.resourceLoader.getExtensions().errors;
41
- if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
42
- const created = await options.host.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent });
43
- return { ...created, services, diagnostics: services.diagnostics };
44
- };
45
- const manager = sessionManager ?? options.host.SessionManager.open(info.path);
46
- const runtime = await options.host.createAgentSessionRuntime(createRuntime, {
47
- cwd: info.cwd,
48
- agentDir: options.agentDir,
49
- sessionManager: manager,
50
- sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile: info.path }
51
- });
52
- const bind = async (session: any) => session.bindExtensions({ mode: "rpc", uiContext: rpcUiContext() });
53
- runtime.setRebindSession(bind);
54
- try { await bind(runtime.session); }
55
- catch (error) { await runtime.dispose().catch(() => undefined); throw error; }
56
- return runtime;
57
- };
58
- }
59
-
24
+ export type { EffectiveSessionOptions } from "./session-effective-options.ts";
25
+ export type SessionCreateResult = { sessionRef: string; effectiveOptions: EffectiveSessionOptions };
26
+ export type CurrentOperationProjection = { operationRef: string; state: "running" | "waiting-approval" | "settling";
27
+ abortable: boolean };
60
28
  export class SessionRuntimeSupervisor {
61
29
  readonly #gatewayInstanceRef: string;
62
30
  readonly #key: Buffer;
@@ -65,13 +33,13 @@ export class SessionRuntimeSupervisor {
65
33
  readonly #runtimeFactory: RuntimeFactory;
66
34
  readonly #events: GatewayEventStore;
67
35
  readonly #host: any | null;
36
+ readonly #operationDeadlinePolicy: SessionOperationDeadlinePolicy;
68
37
  readonly #resolveProject: ((projectRef: string) => string | null) | null;
69
38
  readonly #active = new Map<string, ActiveRuntime>();
70
39
  readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
71
40
  readonly #created = new Map<string, PiSessionInfo>();
72
41
  #closed = false;
73
42
  #readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
74
-
75
43
  constructor(options: {
76
44
  gatewayInstanceRef: string;
77
45
  key: Buffer;
@@ -83,6 +51,7 @@ export class SessionRuntimeSupervisor {
83
51
  packageRoot?: string;
84
52
  modelRuntime?: any;
85
53
  events?: GatewayEventStore;
54
+ operationWatchdog?: SessionOperationWatchdogOptions;
86
55
  resolveProject?(projectRef: string): string | null;
87
56
  }) {
88
57
  this.#gatewayInstanceRef = options.gatewayInstanceRef;
@@ -91,19 +60,17 @@ export class SessionRuntimeSupervisor {
91
60
  this.#listSessions = options.listSessions;
92
61
  this.#events = options.events ?? new GatewayEventStore();
93
62
  this.#host = options.host ?? null;
63
+ this.#operationDeadlinePolicy = sessionOperationDeadlinePolicy(options.operationWatchdog);
94
64
  this.#resolveProject = options.resolveProject ?? null;
95
65
  if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
96
66
  else {
97
67
  if (!options.host || !options.agentDir || !options.packageRoot) throw new Error("session-runtime-factory-unavailable");
98
- this.#runtimeFactory = productionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
68
+ this.#runtimeFactory = createProductionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
99
69
  packageRoot: options.packageRoot, modelRuntime: options.modelRuntime });
100
70
  }
101
71
  }
102
-
103
72
  get activeCount(): number { return this.#active.size; }
104
-
105
73
  setProjectionReader(reader: (sessionRef: string) => Promise<Projection>): void { this.#readProjection = reader; }
106
-
107
74
  async listSessions(): Promise<PiSessionInfo[]> {
108
75
  const persisted = (await this.#listSessions()).filter(isUserConversationSession);
109
76
  const paths = new Set(persisted.map((info) => info.path));
@@ -112,8 +79,11 @@ export class SessionRuntimeSupervisor {
112
79
  }
113
80
  return [...persisted, ...[...this.#created.values()].filter((info) => !paths.has(info.path))];
114
81
  }
115
-
116
82
  async create(projectRef: string, placeRef: string, modelRef: string | null, thinkingLevel: string): Promise<string> {
83
+ return (await this.createWithReadback(projectRef, placeRef, modelRef, thinkingLevel)).sessionRef;
84
+ }
85
+ async createWithReadback(projectRef: string, placeRef: string, modelRef: string | null,
86
+ thinkingLevel: string): Promise<SessionCreateResult> {
117
87
  if (this.#closed || !this.#host) throw new Error("session-create-unavailable");
118
88
  if (placeRef !== projectRef) throw new Error("session-place-mismatch");
119
89
  const sessions = (await this.#listSessions()).filter(isUserConversationSession);
@@ -128,22 +98,19 @@ export class SessionRuntimeSupervisor {
128
98
  const created: PiSessionInfo = { path: sessionFile, id: manager.getSessionId(), cwd: candidates[0], created: createdAt,
129
99
  modified: createdAt, messageCount: 0, firstMessage: "(no messages)", allMessagesText: "" };
130
100
  const sessionRef = sessionRefForPath(this.#key, sessionFile);
131
- await this.#activate(sessionRef, created, manager);
132
- const active = this.#active.get(sessionRef), session = active?.runtime.session;
133
- if (!session) throw new Error("session-runtime-unavailable");
134
- if (modelRef) {
135
- const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
136
- const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
137
- if (!model) throw new Error("session-model-unavailable");
138
- await session.setModel(model);
139
- }
140
- session.setThinkingLevel(thinkingLevel);
101
+ // Retain identity before fallible runtime work so create receipts never point at an orphan.
141
102
  this.#created.set(sessionRef, created);
142
- return sessionRef;
103
+ try { await this.#activate(sessionRef, created, manager); }
104
+ catch {
105
+ return { sessionRef, effectiveOptions: { state: "unknown", modelRef: null, thinkingLevel: null,
106
+ reasonCode: "session-runtime-open-failed" } };
107
+ }
108
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
109
+ if (!session) return { sessionRef, effectiveOptions: { state: "unknown", modelRef: null, thinkingLevel: null,
110
+ reasonCode: "session-runtime-unavailable" } };
111
+ return { sessionRef, effectiveOptions: await configureSessionOptions(session, modelRef, thinkingLevel) };
143
112
  }
144
-
145
113
  liveSessionManager(sessionRef: string): any | null { return this.#active.get(sessionRef)?.sessionManager ?? null; }
146
-
147
114
  async acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
148
115
  if (this.#closed) throw new Error("session-runtime-supervisor-closed");
149
116
  const active = this.#active.get(sessionRef);
@@ -154,7 +121,6 @@ export class SessionRuntimeSupervisor {
154
121
  this.#opening.set(sessionRef, opening);
155
122
  return await opening;
156
123
  }
157
-
158
124
  async #acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
159
125
  if (this.#active.size + this.#opening.size > MAX_WARM_RUNTIMES) throw new Error("session-runtime-capacity");
160
126
  const found = (await this.#listSessions()).filter(isUserConversationSession)
@@ -162,12 +128,11 @@ export class SessionRuntimeSupervisor {
162
128
  if (found.length !== 1) throw new Error(found.length ? "session-ref-ambiguous" : "session-not-found");
163
129
  return await this.#activate(sessionRef, found[0]!);
164
130
  }
165
-
166
131
  async #activate(sessionRef: string, info: PiSessionInfo, sessionManager?: any): Promise<SessionLeaseSnapshot> {
167
132
  if (this.#active.has(sessionRef)) throw new Error("session-owner-conflict");
168
133
  const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
169
134
  const prior = this.#leases.inspect(sessionRef);
170
- if (prior.state === "gateway-owned" || prior.state === "terminal-owned") {
135
+ if (prior.state === "gateway-owned" || prior.state === "terminal-owned" || prior.state === "recovery-required") {
171
136
  try { this.#leases.releaseDeadOwnerForExplicitRecovery(sessionRef); } catch { /* live or unprovable owners stay authoritative */ }
172
137
  }
173
138
  const lease = this.#leases.acquire(sessionRef, this.#gatewayInstanceRef, runtimeInstanceRef);
@@ -179,9 +144,9 @@ export class SessionRuntimeSupervisor {
179
144
  await runtime.dispose().catch(() => undefined);
180
145
  throw new Error("session-owner-continuity-lost");
181
146
  }
182
- const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, unsubscribe: null, completion: null,
147
+ const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, stream: null, unsubscribe: null, completion: null,
183
148
  settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null,
184
- sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null };
149
+ sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null, watchdog: null, lastSessionRevision: null };
185
150
  this.#active.set(sessionRef, active);
186
151
  this.#bindApproval(sessionRef, active);
187
152
  return current;
@@ -191,7 +156,6 @@ export class SessionRuntimeSupervisor {
191
156
  throw error;
192
157
  }
193
158
  }
194
-
195
159
  async release(sessionRef: string): Promise<SessionLeaseSnapshot> {
196
160
  const active = this.#active.get(sessionRef);
197
161
  if (!active) {
@@ -203,7 +167,7 @@ export class SessionRuntimeSupervisor {
203
167
  if (active.settling && active.completion) await active.completion;
204
168
  if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
205
169
  this.#active.delete(sessionRef);
206
- active.unsubscribeApproval?.(); active.unbindApproval?.();
170
+ bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
207
171
  try { await active.runtime.dispose(); }
208
172
  catch (error) {
209
173
  try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
@@ -215,7 +179,28 @@ export class SessionRuntimeSupervisor {
215
179
  if (transient && !(await this.#listSessions()).some((info) => info.path === transient.path)) this.#created.delete(sessionRef);
216
180
  return released;
217
181
  }
218
-
182
+ async restart(sessionRef: string): Promise<SessionLeaseSnapshot> {
183
+ const active = this.#active.get(sessionRef);
184
+ if (!active) return await this.acquire(sessionRef);
185
+ if (active.operationRef || active.settling || !active.runtime.session?.isIdle) throw new Error("session-runtime-busy");
186
+ const info = active.info, manager = active.sessionManager; await this.release(sessionRef);
187
+ return await this.#activate(sessionRef, info, manager ?? undefined);
188
+ }
189
+ currentOperation(sessionRef: string): CurrentOperationProjection | null {
190
+ const active = this.#active.get(sessionRef);
191
+ if (!active?.operationRef) return null;
192
+ return { operationRef: active.operationRef,
193
+ state: active.settling ? "settling" : active.approvalWaiting ? "waiting-approval" : "running",
194
+ abortable: !active.settling };
195
+ }
196
+ currentOperations(): Array<CurrentOperationProjection & { sessionRef: string }> {
197
+ const result: Array<CurrentOperationProjection & { sessionRef: string }> = [];
198
+ for (const sessionRef of this.#active.keys()) {
199
+ const operation = this.currentOperation(sessionRef);
200
+ if (operation) result.push({ sessionRef, ...operation });
201
+ }
202
+ return result;
203
+ }
219
204
  ownership(sessionRef: string): SessionOwnerProjection {
220
205
  const lease = this.#leases.inspect(sessionRef);
221
206
  const active = this.#active.get(sessionRef);
@@ -225,7 +210,7 @@ export class SessionRuntimeSupervisor {
225
210
  reasonCode: null
226
211
  };
227
212
  if (lease.state === "gateway-owned" && active && active.lease.ownerEpoch === lease.ownerEpoch
228
- && lease.gatewayInstanceRef === this.#gatewayInstanceRef) return {
213
+ && lease.gatewayInstanceRef === this.#gatewayInstanceRef && active.lease.runtimeInstanceRef === lease.runtimeInstanceRef) return {
229
214
  state: "gateway-owned", liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : "idle",
230
215
  composerAvailable: true, needsAttention: active.approvalWaiting,
231
216
  owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
@@ -250,10 +235,6 @@ export class SessionRuntimeSupervisor {
250
235
  reasonCode: lease.reasonCode ?? "session-owner-continuity-unknown"
251
236
  };
252
237
  }
253
-
254
- // `message` arrives already carrying any attached document text, and `images`
255
- // already split out: the command controller claims staged refs before it calls
256
- // here, so this layer only forwards what the host prompt takes.
257
238
  async send(sessionRef: string, payload: { delivery: "new-operation" | "follow-up" | "steer"; message: string;
258
239
  expectedOperationRef: string | null; images?: unknown[] }, sessionRevision: string):
259
240
  Promise<{ resultCode: "started" | "queued" | "steered"; operationRef: string }> {
@@ -261,7 +242,7 @@ export class SessionRuntimeSupervisor {
261
242
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
262
243
  if (!active || !session) throw new Error("session-runtime-unavailable");
263
244
  if (payload.delivery !== "new-operation") {
264
- if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || !session.isStreaming) throw new Error("session-operation-conflict");
245
+ if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || active.settling || !session.isStreaming) throw new Error("session-operation-conflict");
265
246
  const images = payload.images?.length ? payload.images : undefined;
266
247
  if (payload.delivery === "follow-up") { await session.followUp(payload.message, images); return { resultCode: "queued", operationRef: active.operationRef }; }
267
248
  await session.steer(payload.message, images); return { resultCode: "steered", operationRef: active.operationRef };
@@ -269,8 +250,14 @@ export class SessionRuntimeSupervisor {
269
250
  if (payload.expectedOperationRef !== null || active.operationRef || active.settling || !session.isIdle) throw new Error("session-operation-conflict");
270
251
  const operationRef = `operation_${randomBytes(24).toString("base64url")}`;
271
252
  const stream = new GatewaySessionStream({ sessionRef, operationRef, events: this.#events });
272
- active.operationRef = operationRef;
273
- active.unsubscribe = session.subscribe((event: unknown) => stream.observe(event));
253
+ const watchdog = new SessionOperationWatchdog(this.#operationDeadlinePolicy);
254
+ active.operationRef = operationRef; active.stream = stream; active.watchdog = watchdog; active.lastSessionRevision = sessionRevision;
255
+ try {
256
+ active.unsubscribe = armSessionOperationWatchdog({ watchdog, subscribe: (listener) => session.subscribe(listener), retrySession: session,
257
+ observe: (event) => stream.observe(event), expire: (reasonCode) => { void this.#terminateOperation(sessionRef,
258
+ operationRef, "error", reasonCode, reasonCode, true).catch(() => undefined); } });
259
+ } catch (error) { stream.markError("session-operation-start-failed");
260
+ await this.#quarantineRuntime(sessionRef, operationRef, active, stream, "session-operation-start-failed"); throw error; }
274
261
  this.#events.publish("runtime.changed", { sessionRef, sessionRevision, liveState: "running", operationRef, reasonCode: null });
275
262
  const created = this.#created.get(sessionRef);
276
263
  if (created && created.firstMessage === "(no messages)") {
@@ -278,35 +265,51 @@ export class SessionRuntimeSupervisor {
278
265
  }
279
266
  let prompt: Promise<void>;
280
267
  try { prompt = session.prompt(payload.message, payload.images?.length ? { images: payload.images } : undefined); }
281
- catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
268
+ catch (error) { stream.markError(); await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
282
269
  const started = waitForOperationStart(stream, prompt);
283
- // A normal prompt resolves after the agent settles. A workflow extension
284
- // command resolves before its nested agent turn starts. In both cases the
285
- // canonical completion boundary is `agent_settled`, not the outer promise.
270
+ // `agent_settled` is canonical even when a workflow's outer prompt resolves first.
286
271
  const lifecycle = started.then(async (mode) => {
287
272
  if (mode === "deferred") await stream.settled();
288
273
  else await Promise.race([stream.settled(), prompt]);
289
274
  });
290
275
  active.completion = lifecycle.then(() => this.#finishOperation(sessionRef, operationRef, stream),
291
- () => this.#finishOperation(sessionRef, operationRef, stream));
276
+ () => { stream.markError(); return this.#finishOperation(sessionRef, operationRef, stream); });
292
277
  try { await started; }
293
278
  catch (error) { await active.completion; throw error; }
294
279
  return { resultCode: "started", operationRef };
295
280
  }
296
-
297
281
  async abort(sessionRef: string, operationRef: string, clearQueued: boolean): Promise<void> {
298
- const active = this.#active.get(sessionRef), session = active?.runtime.session;
299
- if (!active || !session || active.operationRef !== operationRef) throw new Error("session-operation-conflict");
300
- const completion = active.completion;
301
- // Extension hooks do not receive the Agent abort signal while awaiting a
302
- // human approval. Resolve the exact operation's broker promise first so a
303
- // stop request can reach the host instead of waiting for approval expiry.
304
- piApprovalBroker.cancelForOperation(active.info.cwd, active.info.id, operationRef);
305
- await session.abort();
306
- if (clearQueued) session.clearQueue();
307
- await completion;
282
+ await this.#terminateOperation(sessionRef, operationRef, "aborted", "operation-aborted", "operation-abort-timeout", clearQueued);
283
+ }
284
+ async #terminateOperation(sessionRef: string, operationRef: string, settlement: "aborted" | "error",
285
+ reasonCode: string, forcedReasonCode: string, clearQueued: boolean): Promise<void> {
286
+ const active = this.#active.get(sessionRef), session = active?.runtime.session, stream = active?.stream, watchdog = active?.watchdog;
287
+ if (!active || !session || !stream || !watchdog || active.operationRef !== operationRef) throw new Error("session-operation-conflict");
288
+ if (active.settling && !watchdog.terminating) throw new Error("session-operation-conflict");
289
+ active.settling = true; active.approvalWaiting = false;
290
+ const result = await terminateWatchedSessionOperation({ watchdog, settlement, reasonCode, forcedReasonCode, stream,
291
+ completion: () => active.completion,
292
+ settledCleanly: () => this.#active.get(sessionRef) === active && active.operationRef !== operationRef
293
+ && session.isIdle === true && session.isStreaming !== true,
294
+ cancelApproval: (reason) => { piApprovalBroker.cancelForOperation(active.info.cwd, active.info.id, operationRef, reason); },
295
+ abortHost: () => session.abort(), clearQueue: clearQueued ? () => session.clearQueue() : undefined
296
+ });
297
+ if (result.state === "quarantine") await this.#quarantineRuntime(sessionRef, operationRef, active, stream, result.reasonCode);
298
+ else if (this.#active.get(sessionRef) === active) { active.stream = null; active.watchdog = null; active.settling = false; }
299
+ }
300
+ async #quarantineRuntime(sessionRef: string, operationRef: string, active: ActiveRuntime,
301
+ stream: GatewaySessionStream, reasonCode: string): Promise<void> {
302
+ if (this.#active.get(sessionRef) !== active) return;
303
+ bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.watchdog = null;
304
+ if (active.operationRef === operationRef) { active.operationRef = null; stream.complete(null); }
305
+ active.stream = null; active.completion = null; active.settling = false; active.approvalWaiting = false;
306
+ this.#active.delete(sessionRef); bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
307
+ try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
308
+ active.lease.runtimeInstanceRef!, reasonCode); } catch { /* continuity remains fail closed */ }
309
+ if (active.lastSessionRevision) this.#events.publish("runtime.changed", { sessionRef,
310
+ sessionRevision: active.lastSessionRevision, liveState: "uncertain", operationRef: null, reasonCode });
311
+ await boundedResult(Promise.resolve().then(() => active.runtime.dispose()), this.#operationDeadlinePolicy.terminationTimeoutMs);
308
312
  }
309
-
310
313
  #bindApproval(sessionRef: string, active: ActiveRuntime): void {
311
314
  const authority = (): ApprovalAuthority => {
312
315
  if (!active.operationRef || active.lease.state !== "gateway-owned" || !active.lease.revision) return null;
@@ -331,50 +334,53 @@ export class SessionRuntimeSupervisor {
331
334
  active.unbindApproval = piApprovalBroker.bind({ cwd: active.info.cwd, rawSessionId: active.info.id,
332
335
  runtimeInstanceId: active.lease.runtimeInstanceRef!, authority });
333
336
  active.unsubscribeApproval = piApprovalBroker.subscribe(active.info.cwd, active.info.id, (event: ApprovalBrokerEvent) => {
337
+ active.watchdog?.progress();
334
338
  const projection = piApprovalBroker.projection(active.info.cwd, active.info.id);
335
339
  active.approvalWaiting = projection.summary.state === "waiting";
336
340
  void this.#publishApprovalState(sessionRef, active, event);
337
341
  });
338
342
  }
339
-
340
343
  async #publishApprovalState(sessionRef: string, active: ActiveRuntime, event: ApprovalBrokerEvent): Promise<void> {
341
344
  try {
342
345
  const projection = await this.#readProjection?.(sessionRef);
343
346
  if (!projection || this.#active.get(sessionRef) !== active) return;
347
+ active.lastSessionRevision = projection.sessionRevision;
344
348
  this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
345
349
  liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : projection.liveState,
346
350
  operationRef: active.operationRef, reasonCode: `approval-${event.kind}` });
347
351
  } catch { /* approval truth remains available through the canonical inspection route */ }
348
352
  }
349
-
350
353
  async setModel(sessionRef: string, modelRef: string): Promise<"model-changed" | "no-change"> {
351
354
  await this.acquire(sessionRef);
352
355
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
353
356
  if (!active || !session) throw new Error("session-runtime-unavailable");
354
357
  if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
355
- const current = session.model && typeof session.model === "object"
356
- ? webUiModelRef(String(session.model.provider ?? ""), String(session.model.id ?? session.model.modelId ?? "")) : null;
358
+ const current = effectiveModelRef(session);
357
359
  if (current === modelRef) return "no-change";
358
360
  const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
359
- const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
361
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? value.modelId ?? "")) === modelRef);
360
362
  if (!model) throw new Error("session-model-unavailable");
361
363
  await session.setModel(model);
364
+ const effective = effectiveModelRef(session);
365
+ if (!effective) throw new Error("session-model-effect-unknown");
366
+ if (effective !== modelRef) throw new Error("session-model-mismatch");
362
367
  this.#touchCreated(sessionRef);
363
368
  return "model-changed";
364
369
  }
365
-
366
370
  async setThinking(sessionRef: string, thinkingLevel: string): Promise<"thinking-changed" | "no-change"> {
367
371
  await this.acquire(sessionRef);
368
372
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
369
373
  if (!active || !session) throw new Error("session-runtime-unavailable");
370
374
  if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
371
- const current = String(session.thinkingLevel ?? session.getThinkingLevel?.() ?? "unknown");
375
+ const current = effectiveThinkingLevel(session);
372
376
  if (current === thinkingLevel) return "no-change";
373
- session.setThinkingLevel(thinkingLevel);
377
+ await session.setThinkingLevel(thinkingLevel);
378
+ const effective = effectiveThinkingLevel(session);
379
+ if (!effective) throw new Error("session-thinking-effect-unknown");
380
+ if (effective !== thinkingLevel) throw new Error("session-thinking-mismatch");
374
381
  this.#touchCreated(sessionRef);
375
382
  return "thinking-changed";
376
383
  }
377
-
378
384
  async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
379
385
  await this.acquire(sessionRef);
380
386
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
@@ -384,7 +390,6 @@ export class SessionRuntimeSupervisor {
384
390
  this.#touchCreated(sessionRef);
385
391
  return "permission-changed";
386
392
  }
387
-
388
393
  async runRuntimeCommand(sessionRef: string, command: string): Promise<RuntimeCommandResult> {
389
394
  await this.acquire(sessionRef);
390
395
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
@@ -398,12 +403,10 @@ export class SessionRuntimeSupervisor {
398
403
  health: { state: "unavailable", reasonCode: "approval-broker-unavailable", message: "Approval broker is unavailable" } } };
399
404
  return piApprovalBroker.projection(active.info.cwd, active.info.id);
400
405
  }
401
-
402
406
  approvalDetail(sessionRef: string, approvalRef: string): unknown | null {
403
407
  const active = this.#active.get(sessionRef);
404
408
  return active ? piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef) : null;
405
409
  }
406
-
407
410
  async decideApproval(approvalRef: string, decision: unknown): Promise<unknown> {
408
411
  for (const active of this.#active.values()) {
409
412
  if (piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef)) {
@@ -412,7 +415,6 @@ export class SessionRuntimeSupervisor {
412
415
  }
413
416
  throw new Error("approval-not-pending");
414
417
  }
415
-
416
418
  async rename(sessionRef: string, title: string): Promise<"renamed" | "no-change"> {
417
419
  await this.acquire(sessionRef);
418
420
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
@@ -424,7 +426,6 @@ export class SessionRuntimeSupervisor {
424
426
  this.#touchCreated(sessionRef);
425
427
  return "renamed";
426
428
  }
427
-
428
429
  async fork(sessionRef: string, entryRef: string | null, title: string | null): Promise<string> {
429
430
  if (!this.#host) throw new Error("session-fork-unavailable");
430
431
  await this.acquire(sessionRef);
@@ -451,28 +452,43 @@ export class SessionRuntimeSupervisor {
451
452
  this.#created.set(forkRef, created);
452
453
  return forkRef;
453
454
  }
454
-
455
- #touchCreated(sessionRef: string): void {
456
- const created = this.#created.get(sessionRef);
457
- if (created) created.modified = new Date();
458
- }
459
-
455
+ #touchCreated(sessionRef: string): void { const created = this.#created.get(sessionRef); if (created) created.modified = new Date(); }
460
456
  async #finishOperation(sessionRef: string, operationRef: string, stream: GatewaySessionStream): Promise<void> {
461
457
  const active = this.#active.get(sessionRef);
462
458
  if (!active || active.operationRef !== operationRef) return;
463
- active.unsubscribe?.(); active.unsubscribe = null; active.operationRef = null; active.settling = true;
459
+ const restartRequired = stream.runtimeRestartRequired; let projection: Projection | null = null;
460
+ bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.settling = true;
464
461
  try {
465
- const projection = await this.#readProjection?.(sessionRef);
466
- if (!projection) return;
467
- stream.complete(projection.sessionRevision);
468
- this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
469
- liveState: projection.liveState, operationRef: null, reasonCode: null });
462
+ if (this.#readProjection) {
463
+ const read = await boundedResult(this.#readProjection(sessionRef), this.#operationDeadlinePolicy.projectionTimeoutMs);
464
+ if (read.state === "settled") projection = read.value;
465
+ else if (read.state === "timeout") stream.markError("session-projection-timeout");
466
+ }
470
467
  } catch { /* Canonical refresh failure cannot be replaced by an invented revision. */ }
471
- finally {
472
- if (this.#active.get(sessionRef) === active) { active.settling = false; active.completion = null; }
468
+ if (this.#active.get(sessionRef) !== active || active.operationRef !== operationRef) return;
469
+ if (this.ownership(sessionRef).state !== "gateway-owned") { stream.markError("session-owner-continuity-lost"); await this.#quarantineRuntime(sessionRef, operationRef, active, stream, "session-owner-continuity-lost"); return; }
470
+ // The pre-clear projection is stale; derive post-settlement liveness from exact ownership.
471
+ active.operationRef = null; const settledLiveState = this.ownership(sessionRef).liveState;
472
+ if (projection) active.lastSessionRevision = projection.sessionRevision;
473
+ const taskOutcome = activeSessionTask(active.info.cwd, active.info.id)?.trace?.outcome ?? null;
474
+ stream.complete(projection?.sessionRevision ?? null, taskOutcome);
475
+ if (projection) this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
476
+ liveState: restartRequired ? "uncertain" : settledLiveState, operationRef: null,
477
+ reasonCode: restartRequired ? "runtime-restart-required" : null });
478
+ if (this.#active.get(sessionRef) === active) { active.completion = null;
479
+ if (!active.watchdog?.terminating) { active.stream = null; active.watchdog = null; active.settling = false; } }
480
+ if (!restartRequired || !projection || this.#active.get(sessionRef) !== active) return;
481
+ let restartAuthority: ActiveRuntime | null = active;
482
+ try {
483
+ await this.restart(sessionRef); restartAuthority = this.#active.get(sessionRef) ?? null;
484
+ const projection = await this.#readProjection?.(sessionRef);
485
+ if (!projection || !restartAuthority || this.#active.get(sessionRef) !== restartAuthority || this.ownership(sessionRef).state !== "gateway-owned") return;
486
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision, liveState: projection.liveState, operationRef: null, reasonCode: null });
487
+ } catch {
488
+ if (!restartAuthority || this.#active.get(sessionRef) !== restartAuthority || this.ownership(sessionRef).state !== "gateway-owned") return;
489
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision, liveState: "uncertain", operationRef: null, reasonCode: "runtime-restart-failed" });
473
490
  }
474
491
  }
475
-
476
492
  async close(): Promise<void> {
477
493
  if (this.#closed) return;
478
494
  this.#closed = true;
@@ -237,8 +237,11 @@ export class SessionLeaseStore {
237
237
  if (!Number.isFinite(now.getTime())) throw new Error("session-lease-input-invalid");
238
238
  return this.#withMutationLock(sessionRef, () => {
239
239
  const current = this.inspect(sessionRef);
240
- const pid = current.state === "terminal-owned" ? terminalOwnerPid(current.gatewayInstanceRef)
241
- : current.state === "gateway-owned" ? gatewayOwnerPid(current.gatewayInstanceRef) : null;
240
+ const terminalOwner = current.state === "terminal-owned"
241
+ || (current.state === "recovery-required" && current.gatewayInstanceRef?.startsWith(TERMINAL_OWNER_PREFIX));
242
+ const pid = terminalOwner ? terminalOwnerPid(current.gatewayInstanceRef)
243
+ : current.state === "gateway-owned" || current.state === "recovery-required"
244
+ ? gatewayOwnerPid(current.gatewayInstanceRef) : null;
242
245
  if (!pid || processAlive(pid)) throw new Error("session-owner-not-proven-dead");
243
246
  this.#append(sessionRef, { recordedAt: now.toISOString(), sessionRef, event: "recovery-required", ownerEpoch: current.ownerEpoch!,
244
247
  gatewayInstanceRef: current.gatewayInstanceRef!, runtimeInstanceRef: current.runtimeInstanceRef!, reasonCode: "owner-process-exited" });
@@ -236,6 +236,7 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
236
236
  }
237
237
  async transcript(beforeCursor: string | null, limit: number): Promise<unknown> {
238
238
  const snapshot = await this.snapshot() as any;
239
+ const activeTask = this.#input.task?.() as any;
239
240
  return projectTranscript({ identity: { projectRef: snapshot.identity.projectRef, runtimeInstanceId: snapshot.identity.runtimeInstanceId,
240
241
  sessionRef: snapshot.identity.sessionRef, taskId: snapshot.identity.taskId, taskRunId: snapshot.identity.taskRunId, agentOperationId: null, toolCallId: null },
241
242
  revision: { runtimeRevision: snapshot.revision.runtimeRevision, taskRevision: snapshot.revision.taskRevision,
@@ -243,7 +244,7 @@ export class CoreInspectionProvider implements WebUiReadModelProvider {
243
244
  indexRevision: snapshot.revision.indexRevision, approvalRevision: snapshot.revision.approvalRevision,
244
245
  sessionOptionRevision: snapshot.revision.sessionOptionRevision, queueRevision: snapshot.revision.queueRevision },
245
246
  eventCursor: snapshot.revision.eventCursor, entries: this.#input.sessionEntries?.() ?? [], beforeCursor, limit,
246
- generatedAt: snapshot.generatedAt });
247
+ generatedAt: snapshot.generatedAt, taskOutcome: typeof activeTask?.trace?.outcome === "string" ? activeTask.trace.outcome : null });
247
248
  }
248
249
  async queue(): Promise<unknown> {
249
250
  if (!this.#input.queueProjection) throw new ReadModelNotFound();
@@ -56,6 +56,7 @@ export async function startLoopbackServer(options: {
56
56
  mode?: "session" | "gateway";
57
57
  readCapabilities: ReadCapabilities;
58
58
  readSessionCatalog?: () => unknown | Promise<unknown>;
59
+ readSessionLiveState?: () => unknown | Promise<unknown>;
59
60
  readSessionCreationOptions?: () => unknown | Promise<unknown>;
60
61
  readSessionModel?: (sessionRef: string) => WebUiReadModelProvider | Promise<WebUiReadModelProvider>;
61
62
  executeSessionAttachment?: (sessionRef: string, command: unknown) => unknown | Promise<unknown>;
@@ -296,6 +297,13 @@ export async function startLoopbackServer(options: {
296
297
  catch { return errorResponse(response, 503, "session-catalog-unavailable"); }
297
298
  }
298
299
 
300
+ if (request.method === "GET" && url.pathname === "/api/v1/session-live-state" && options.readSessionLiveState) {
301
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
302
+ if (url.search) return errorResponse(response, 400, "invalid-session-live-state-query");
303
+ try { return jsonResponse(response, 200, await options.readSessionLiveState()); }
304
+ catch { return errorResponse(response, 503, "session-live-state-unavailable"); }
305
+ }
306
+
299
307
  if (request.method === "GET" && url.pathname === "/api/v1/session-creation-options" && options.readSessionCreationOptions) {
300
308
  if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
301
309
  if (url.search) return errorResponse(response, 400, "invalid-session-creation-query");