@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
@@ -7,7 +7,7 @@
7
7
  <meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
8
8
  <meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
9
9
  <title>Piagent WebUI</title>
10
- <script type="module" crossorigin src="/assets/index-BZRFw1u6.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-CgOEyRPG.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-BV-_03yl.css">
12
12
  </head>
13
13
  <body>
@@ -446,7 +446,9 @@ export default function piagentWebUiExtension(pi: ExtensionAPI): void {
446
446
  pi.on("tool_execution_end", (event, ctx) => { publishDrafts(ctx, stream.toolEnded(event, bridge.snapshot())); bridgeSoft(() => lifecycle.observeToolEnd(ctx)); });
447
447
  pi.on("turn_end", (event, ctx) => publishDrafts(ctx, stream.turnEnded(event, bridge.snapshot())));
448
448
  pi.on("agent_settled", (_event, ctx) => {
449
- publishDrafts(ctx, stream.agentSettled(bridge.snapshot(), typeof ctx.hasPendingMessages === "function" ? ctx.hasPendingMessages() : null));
449
+ const hasPendingMessages = typeof ctx.hasPendingMessages === "function" ? ctx.hasPendingMessages() : null;
450
+ publishDrafts(ctx, stream.agentSettled(bridge.snapshot(), hasPendingMessages));
451
+ if (hasPendingMessages === true) { publishPersisted(ctx); return; }
450
452
  const operationId = bridge.snapshot().identity?.agentOperationId ?? null;
451
453
  bridgeSoft(() => bridge.observeAgentSettled(ctx)); bridgeSoft(() => lifecycle.observeAgentSettled(ctx, operationId)); publishPersisted(ctx);
452
454
  });
@@ -317,7 +317,7 @@ export class SameSessionPiBridge {
317
317
  }
318
318
  observeAgentStart(ctx: ExtensionContext): void {
319
319
  if (!this.refresh(ctx) || !this.#binding || this.#binding.state !== "ready") return;
320
- const pending = this.#pending;
320
+ const pending = this.#pending; if (this.#binding.identity.agentOperationId && !pending) return;
321
321
  const operationId = pending?.operationId ?? this.#revision("operation", ++this.#operationCounter, "external");
322
322
  this.#binding.identity.agentOperationId = operationId; this.#refresh(); this.#event("operation.started", { operationRef: operationId, commandId: pending?.command.commandId });
323
323
  const observedEntry = pending ? dispatchEntry(pending, ctx) : null;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
4
+ import { classifyToolFailure, handledToolFailure } from "../../piagent-core/runtime/inspection/tool-failure-classification.ts";
4
5
  import type { RuntimeEventDraft, RuntimeEventRevision } from "../../piagent-core/runtime/inspection/runtime-event-store.ts";
5
6
  import type { BridgeIdentity, BridgeSnapshot } from "./same-session-bridge.ts";
6
7
 
@@ -35,6 +36,13 @@ function safeToolName(value: unknown): string {
35
36
  if (projected.redacted) return "redacted-tool";
36
37
  return projected.text.replace(/[^A-Za-z0-9._:-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 128) || "tool";
37
38
  }
39
+ function toolClassificationInput(value: unknown): { command: string } | undefined {
40
+ if (!value || typeof value !== "object") return undefined;
41
+ const direct = (value as { command?: unknown }).command;
42
+ const nested = (value as { args?: { command?: unknown } }).args?.command;
43
+ const command = typeof direct === "string" ? direct : typeof nested === "string" ? nested : null;
44
+ return command === null ? undefined : { command: command.slice(0, 8_192) };
45
+ }
38
46
  function role(message: any): MessageRole | null {
39
47
  return message?.role === "user" ? "user" : message?.role === "assistant" ? "assistant" : message?.role === "toolResult" ? "tool-result" : null;
40
48
  }
@@ -68,10 +76,11 @@ function redaction(applied = false, truncated = false) { return { applied, value
68
76
  function identityReady(identity: BridgeIdentity | null): identity is BridgeIdentity & { agentOperationId: string } {
69
77
  return Boolean(identity?.agentOperationId);
70
78
  }
71
- function activityPayload(state: "started" | "progress" | "finished" | "failed", activityRef: string, toolName: string) {
79
+ function activityPayload(state: "started" | "progress" | "finished" | "failed", activityRef: string, toolName: string,
80
+ reasonCode: string | null = null) {
72
81
  return { state, activityType: "tool", activityRef, toolName, inputDigest: null, outputDigest: null, preview: null, previewKind: "none",
73
82
  outputBytes: null, outputLines: null, exitCode: null, isError: state === "finished" ? false : state === "failed" ? true : null,
74
- affectedFileRefs: [], criterionIds: [], verifierAttemptIds: [], reasonCode: state === "failed" ? "tool-execution-failed" : null };
83
+ affectedFileRefs: [], criterionIds: [], verifierAttemptIds: [], reasonCode };
75
84
  }
76
85
 
77
86
  export class PiSessionStreamAdapter {
@@ -81,12 +90,13 @@ export class PiSessionStreamAdapter {
81
90
  #activeByRole = new Map<MessageRole, ActiveMessage>();
82
91
  #lastMessageRef: string | null = null;
83
92
  #toolRefs = new Map<string, string>();
93
+ #toolInputs = new Map<string, { command: string } | undefined>();
84
94
  #lastToolProgress = new Map<string, number>();
85
95
 
86
96
  constructor(options: { now?: () => Date } = {}) { this.#now = options.now ?? (() => new Date()); }
87
97
  reset(): void {
88
98
  this.#turnIndex = null; this.#messageCounter = 0; this.#activeByRole.clear(); this.#lastMessageRef = null;
89
- this.#toolRefs.clear(); this.#lastToolProgress.clear();
99
+ this.#toolRefs.clear(); this.#toolInputs.clear(); this.#lastToolProgress.clear();
90
100
  }
91
101
 
92
102
  #base(snapshot: BridgeSnapshot, sourceObservedAt?: unknown): Omit<RuntimeEventDraft, "kind" | "payload" | "evidence" | "redaction"> | null {
@@ -210,13 +220,14 @@ export class PiSessionStreamAdapter {
210
220
  const draft = this.#draft(snapshot, "turn.ended", { phase: "ended", finalMessageRef: this.#lastMessageRef, toolResultCount: count, stopReason: reason });
211
221
  return draft ? [draft] : [];
212
222
  }
213
- toolStarted(event: { toolCallId?: unknown; toolName?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
223
+ toolStarted(event: { toolCallId?: unknown; toolName?: unknown; args?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
214
224
  if (typeof event.toolCallId !== "string" || !event.toolCallId) return [];
215
225
  const drafts = this.#activeByRole.get("assistant")
216
226
  ? this.#drainLines(snapshot, this.#activeByRole.get("assistant") as ActiveMessage, false) : [];
217
227
  const name = safeToolName(event.toolName);
218
228
  const ref = opaque("tool", [snapshot.identity?.sessionRef, event.toolCallId]), activityRef = opaque("activity", [snapshot.identity?.sessionRef, event.toolCallId]);
219
- this.#toolRefs.set(event.toolCallId, ref); this.#lastToolProgress.set(event.toolCallId, 0);
229
+ this.#toolRefs.set(event.toolCallId, ref); this.#toolInputs.set(event.toolCallId, toolClassificationInput(event.args));
230
+ this.#lastToolProgress.set(event.toolCallId, 0);
220
231
  const draft = this.#draft(snapshot, "activity.started", activityPayload("started", activityRef, name), { toolCallId: ref });
221
232
  if (draft) drafts.push(draft);
222
233
  return drafts;
@@ -231,19 +242,31 @@ export class PiSessionStreamAdapter {
231
242
  const draft = this.#draft(snapshot, "activity.progress", activityPayload("progress", activityRef, name), { toolCallId: ref });
232
243
  return draft ? [draft] : [];
233
244
  }
234
- toolEnded(event: { toolCallId?: unknown; toolName?: unknown; isError?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
245
+ toolEnded(event: { toolCallId?: unknown; toolName?: unknown; result?: unknown; isError?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
235
246
  if (typeof event.toolCallId !== "string") return [];
236
247
  const ref = this.#toolRefs.get(event.toolCallId); if (!ref) return [];
237
248
  const name = safeToolName(event.toolName);
238
- const activityRef = opaque("activity", [snapshot.identity?.sessionRef, event.toolCallId]), state = event.isError === true ? "failed" : "finished";
239
- this.#toolRefs.delete(event.toolCallId); this.#lastToolProgress.delete(event.toolCallId);
240
- const draft = this.#draft(snapshot, `activity.${state}`, activityPayload(state, activityRef, name), { toolCallId: ref });
249
+ const reasonCode = classifyToolFailure(name, event.isError === true, event.result, this.#toolInputs.get(event.toolCallId));
250
+ const state = (event.isError === true || reasonCode !== null) && !handledToolFailure(reasonCode) ? "failed" : "finished";
251
+ const activityRef = opaque("activity", [snapshot.identity?.sessionRef, event.toolCallId]);
252
+ this.#toolRefs.delete(event.toolCallId); this.#toolInputs.delete(event.toolCallId); this.#lastToolProgress.delete(event.toolCallId);
253
+ const draft = this.#draft(snapshot, `activity.${state}`, activityPayload(state, activityRef, name, reasonCode), { toolCallId: ref });
241
254
  return draft ? [draft] : [];
242
255
  }
243
256
  agentSettled(snapshot: BridgeSnapshot, hasPendingMessages: boolean | null): RuntimeEventDraft[] {
257
+ // A queued follow-up belongs to the same logical operation. Do not emit a
258
+ // terminal edge or clear stream identity between recovery turns.
259
+ if (hasPendingMessages === true) return [];
244
260
  const value = hasPendingMessages === null ? { state: "unknown", value: null, reasonCode: "queue-fact-unavailable" }
245
261
  : { state: "known", value: hasPendingMessages, reasonCode: null };
246
- const draft = this.#draft(snapshot, "agent-operation.settled", { settlement: "completed", lastStopReason: null, hasPendingMessages: value });
262
+ // Host-idle only proves that this agent operation stopped producing turns.
263
+ // A governed task is successful only after its durable contract is terminal.
264
+ // Fail closed here so a missed/late completion hook cannot paint an open
265
+ // task as completed in the WebUI.
266
+ const governedTaskOpen = Boolean(snapshot.identity?.taskRunId) && snapshot.taskState !== "terminal";
267
+ const draft = this.#draft(snapshot, "agent-operation.settled", {
268
+ settlement: governedTaskOpen ? "unknown" : "completed", lastStopReason: null, hasPendingMessages: value
269
+ });
247
270
  this.reset(); return draft ? [draft] : [];
248
271
  }
249
272
  }
@@ -1,5 +1,7 @@
1
+ import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
2
+
1
3
  export type GatewayEventKind = "catalog.changed" | "session.changed" | "runtime.changed" | "message.delta"
2
- | "message.completed" | "tool.started" | "tool.completed";
4
+ | "message.completed" | "operation.settled" | "tool.started" | "tool.completed";
3
5
 
4
6
  export type GatewayProtocolEvent = {
5
7
  schemaVersion: 1;
@@ -12,16 +14,47 @@ export type GatewayProtocolEvent = {
12
14
  payload: Record<string, unknown>;
13
15
  };
14
16
 
17
+ export type GatewayTerminalSettlement = {
18
+ sessionRef: string;
19
+ operationRef: string;
20
+ settlement: "blocked" | "aborted" | "error" | "unknown";
21
+ reasonCode: string;
22
+ settledAt: string;
23
+ sequence: number;
24
+ };
25
+
26
+ const OPAQUE_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
27
+ const REASON_CODE_PATTERN = /^[a-z0-9][a-z0-9.-]{0,95}$/;
28
+ const TERMINAL_SETTLEMENTS = new Set(["completed", "blocked", "aborted", "error", "unknown"]);
29
+ const SETTLEMENT_RETENTION_COUNT = 100;
30
+ const SETTLEMENT_RETENTION_MS = 60 * 60_000;
31
+ const MAX_SETTLEMENT_DECISION_COUNT = 10_000;
32
+ type GatewaySettlementDecision = { settledAt: string; settlement: "completed" | GatewayTerminalSettlement["settlement"];
33
+ reasonCode: string | null };
34
+
35
+ function settlementReason(settlement: GatewayTerminalSettlement["settlement"], value: unknown): string {
36
+ if (typeof value === "string" && REASON_CODE_PATTERN.test(value) && !redactSensitiveText(value).redacted) return value;
37
+ if (settlement === "aborted") return "operation-aborted";
38
+ if (settlement === "blocked") return "operation-blocked";
39
+ if (settlement === "error") return "operation-failed";
40
+ return "operation-settlement-unknown";
41
+ }
42
+
15
43
  export class GatewayEventStore {
16
44
  readonly #maximumCount: number;
17
45
  readonly #maximumAgeMs: number;
46
+ readonly #settlementDecisionCount: number;
18
47
  #events: GatewayProtocolEvent[] = [];
48
+ #settlements: GatewayTerminalSettlement[] = [];
49
+ #settlementDecisions = new Map<string, GatewaySettlementDecision>();
19
50
  #sequence = 0;
20
51
  #listeners = new Set<(event: GatewayProtocolEvent) => void>();
21
52
 
22
- constructor(options: { maximumCount?: number; maximumAgeMs?: number } = {}) {
53
+ constructor(options: { maximumCount?: number; maximumAgeMs?: number; settlementDecisionCount?: number } = {}) {
23
54
  this.#maximumCount = Math.max(1, Math.min(10_000, options.maximumCount ?? 1_000));
24
55
  this.#maximumAgeMs = Math.max(1_000, Math.min(24 * 60 * 60_000, options.maximumAgeMs ?? 60 * 60_000));
56
+ this.#settlementDecisionCount = Math.max(1,
57
+ Math.min(MAX_SETTLEMENT_DECISION_COUNT, Math.floor(options.settlementDecisionCount ?? MAX_SETTLEMENT_DECISION_COUNT)));
25
58
  }
26
59
 
27
60
  get stateVersion(): number { return this.#sequence; }
@@ -31,14 +64,74 @@ export class GatewayEventStore {
31
64
  while (this.#events.length > this.#maximumCount || this.#events[0] && Date.parse(this.#events[0].generatedAt) < cutoff) this.#events.shift();
32
65
  }
33
66
 
67
+ #pruneSettlements(now: number): void {
68
+ const cutoff = now - SETTLEMENT_RETENTION_MS;
69
+ this.#settlements = this.#settlements.filter((item) => Date.parse(item.settledAt) >= cutoff);
70
+ if (this.#settlements.length > SETTLEMENT_RETENTION_COUNT) {
71
+ this.#settlements.splice(0, this.#settlements.length - SETTLEMENT_RETENTION_COUNT);
72
+ }
73
+ for (const [key, decision] of this.#settlementDecisions) {
74
+ if (Date.parse(decision.settledAt) < cutoff) this.#settlementDecisions.delete(key);
75
+ }
76
+ while (this.#settlementDecisions.size > this.#settlementDecisionCount) {
77
+ this.#settlementDecisions.delete(this.#settlementDecisions.keys().next().value as string);
78
+ }
79
+ }
80
+
81
+ #settlementDecision(sessionRef: string, operationRef: string): GatewaySettlementDecision | undefined {
82
+ const key = `${sessionRef}\u0000${operationRef}`;
83
+ const indexed = this.#settlementDecisions.get(key);
84
+ if (indexed) return indexed;
85
+ const retained = this.#settlements.find((item) => item.sessionRef === sessionRef && item.operationRef === operationRef);
86
+ if (!retained) return undefined;
87
+ const restored = { settledAt: retained.settledAt, settlement: retained.settlement, reasonCode: retained.reasonCode };
88
+ this.#settlementDecisions.set(key, restored);
89
+ return restored;
90
+ }
91
+
92
+ #recordSettlement(event: GatewayProtocolEvent, now: number): void {
93
+ if (event.kind !== "operation.settled") return;
94
+ const sessionRef = event.payload.sessionRef, operationRef = event.payload.operationRef, settlement = event.payload.settlement;
95
+ if (typeof sessionRef !== "string" || !OPAQUE_REF_PATTERN.test(sessionRef)
96
+ || typeof operationRef !== "string" || !OPAQUE_REF_PATTERN.test(operationRef)
97
+ || typeof settlement !== "string" || !TERMINAL_SETTLEMENTS.has(settlement)) return;
98
+ this.#pruneSettlements(now);
99
+ const key = `${sessionRef}\u0000${operationRef}`;
100
+ if (this.#settlementDecision(sessionRef, operationRef)) return;
101
+ this.#settlementDecisions.set(key, { settledAt: event.generatedAt,
102
+ settlement: settlement as GatewaySettlementDecision["settlement"],
103
+ reasonCode: settlement === "completed" ? null : String(event.payload.reasonCode) });
104
+ const retained = { sessionRef, operationRef, settledAt: event.generatedAt, sequence: event.sequence };
105
+ if (settlement !== "completed") {
106
+ const outcome = settlement as GatewayTerminalSettlement["settlement"];
107
+ this.#settlements.push({ ...retained, settlement: outcome, reasonCode: settlementReason(outcome, event.payload.reasonCode) });
108
+ }
109
+ this.#pruneSettlements(now);
110
+ }
111
+
34
112
  publish(kind: GatewayEventKind, payload: Record<string, unknown>, now = new Date()): GatewayProtocolEvent {
35
113
  if (!Number.isFinite(now.getTime())) throw new Error("gateway-event-time-invalid");
114
+ if (kind === "operation.settled") {
115
+ if (payload.settlement === "completed") payload = { ...payload, reasonCode: null };
116
+ else {
117
+ const settlement = ["blocked", "aborted", "error", "unknown"].includes(String(payload.settlement))
118
+ ? payload.settlement as GatewayTerminalSettlement["settlement"] : "unknown";
119
+ payload = { ...payload, settlement, reasonCode: settlementReason(settlement, payload.reasonCode) };
120
+ }
121
+ this.#pruneSettlements(now.getTime());
122
+ const sessionRef = payload.sessionRef, operationRef = payload.operationRef;
123
+ if (typeof sessionRef === "string" && OPAQUE_REF_PATTERN.test(sessionRef)
124
+ && typeof operationRef === "string" && OPAQUE_REF_PATTERN.test(operationRef)) {
125
+ const canonical = this.#settlementDecision(sessionRef, operationRef);
126
+ if (canonical) payload = { ...payload, settlement: canonical.settlement, reasonCode: canonical.reasonCode };
127
+ }
128
+ }
36
129
  this.#sequence += 1;
37
130
  const event: GatewayProtocolEvent = {
38
131
  schemaVersion: 1, version: "piagent-gateway-protocol-v1", messageType: "event",
39
132
  sequence: this.#sequence, stateVersion: this.#sequence, generatedAt: now.toISOString(), kind, payload
40
133
  };
41
- this.#events.push(event); this.#prune(now.getTime());
134
+ this.#events.push(event); this.#prune(now.getTime()); this.#recordSettlement(event, now.getTime());
42
135
  for (const listener of this.#listeners) { try { listener(event); } catch { /* observers cannot block truth */ } }
43
136
  return event;
44
137
  }
@@ -48,10 +141,21 @@ export class GatewayEventStore {
48
141
  return () => { this.#listeners.delete(listener); };
49
142
  }
50
143
 
144
+ recentOperationSettlements(maximum = 100, now = new Date()): GatewayTerminalSettlement[] {
145
+ if (!Number.isFinite(now.getTime())) throw new Error("gateway-settlement-time-invalid");
146
+ const limit = Math.max(0, Math.min(100, Math.floor(maximum)));
147
+ if (limit === 0) return [];
148
+ this.#pruneSettlements(now.getTime());
149
+ return [...this.#settlements].sort((left, right) => right.sequence - left.sequence).slice(0, limit).map((item) => ({ ...item }));
150
+ }
151
+
51
152
  replay(after: number | null): { state: "current" | "resync-required"; events: GatewayProtocolEvent[]; earliestSequence: number; currentSequence: number } {
52
153
  this.#prune(Date.now());
53
- const earliestSequence = this.#events[0]?.sequence ?? this.#sequence;
54
- if (after !== null && after < Math.max(0, earliestSequence - 1)) {
154
+ // An empty retained ring after pruning contains no event at `#sequence`.
155
+ // Treat the next sequence as the retention boundary so a client one event
156
+ // behind must resync instead of silently receiving an empty replay.
157
+ const earliestSequence = this.#events[0]?.sequence ?? Math.min(Number.MAX_SAFE_INTEGER, this.#sequence + 1);
158
+ if (after !== null && (after > this.#sequence || after < Math.max(0, earliestSequence - 1))) {
55
159
  return { state: "resync-required", events: [], earliestSequence, currentSequence: this.#sequence };
56
160
  }
57
161
  return { state: "current", events: after === null ? [] : this.#events.filter((event) => event.sequence > after), earliestSequence,
@@ -21,6 +21,7 @@ import { SessionMetadataStore } from "./session-metadata-store.ts";
21
21
  import { GatewayProtocolService } from "./gateway-protocol-service.ts";
22
22
  import { SessionLeaseStore } from "./session-lease-store.ts";
23
23
  import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
24
+ import { buildSessionLiveState } from "./session-live-state.ts";
24
25
  import { SessionCommandStore } from "./session-command-store.ts";
25
26
  import { SessionCommandController } from "./session-command-controller.ts";
26
27
  import { RuntimeCommandController } from "./runtime-command-controller.ts";
@@ -192,6 +193,8 @@ export async function startPiagentGateway(options: {
192
193
  mode: "gateway",
193
194
  readCapabilities: () => capabilities(gatewayInstanceRef, true),
194
195
  readSessionCatalog: readCatalog,
196
+ readSessionLiveState: () => buildSessionLiveState({ gatewayInstanceRef, eventSequence: events.stateVersion,
197
+ operations: runtimes!.currentOperations(), settlements: events.recentOperationSettlements() }),
195
198
  readSessionCreationOptions: () => inspections.creationOptions(),
196
199
  readSessionModel: (sessionRef) => inspections.provider(sessionRef),
197
200
  readSessionConnections: (sessionRef) => inspections.connections(sessionRef),
@@ -1,16 +1,24 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
 
3
3
  import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
4
+ import { hasVisibleText } from "../shared/text-visibility.ts";
4
5
  import { GatewayEventStore } from "./gateway-events.ts";
6
+ import { SessionOperationLifecycle, type SessionOperationObservation,
7
+ type SessionOperationPhase, type SessionOperationRetryPolicyOptions } from "./session-operation-lifecycle.ts";
5
8
 
6
9
  const MAX_BUFFER = 65_536;
7
10
  const MAX_EVENTS = 128;
8
11
  const MAX_DELTA = 16_384;
9
12
  const PRIVATE_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
10
13
  const PRIVATE_END = /-----END [A-Z ]*PRIVATE KEY-----/;
14
+ const RUNTIME_DRIFT = /Installed Piagent runtime changed (?:during this session|while the capability lock was being re-pinned)/i;
15
+ const COMPLETION_GATE = /^\[Piagent completion gate: (CONTINUING|NOT APPROVED)\]/i;
16
+
17
+ export type GatewayOperationSettlement = "completed" | "blocked" | "aborted" | "error" | "unknown";
18
+ type Settlement = { outcome: GatewayOperationSettlement; reasonCode: string | null };
11
19
 
12
20
  function clean(value: unknown): string {
13
- return String(value ?? "").replace(/\u001b(?:[@-_]|\[[0-?]*[ -/]*[@-~])/g, "")
21
+ return String(value ?? "").replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g, "")
14
22
  .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
15
23
  }
16
24
  function safeTool(value: unknown): string {
@@ -19,6 +27,44 @@ function safeTool(value: unknown): string {
19
27
  return safe.text.replace(/[^A-Za-z0-9._:-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 160) || "tool";
20
28
  }
21
29
  function ref(prefix: string): string { return `${prefix}_${randomBytes(18).toString("base64url")}`; }
30
+ function messageText(message: any): string {
31
+ if (typeof message?.content === "string") return message.content;
32
+ if (!Array.isArray(message?.content)) return "";
33
+ return message.content.filter((item: any) => item?.type === "text" && typeof item.text === "string")
34
+ .map((item: any) => item.text).join("\n");
35
+ }
36
+ function assistantSettlement(message: any): Settlement {
37
+ const stopReason = String(message?.stopReason ?? "");
38
+ if (stopReason === "aborted") return { outcome: "aborted", reasonCode: "operation-aborted" };
39
+ if (stopReason === "error") return { outcome: "error", reasonCode: "assistant-response-failed" };
40
+ if (stopReason === "length") return { outcome: "error", reasonCode: "assistant-output-incomplete" };
41
+ const gate = COMPLETION_GATE.exec(messageText(message).trimStart());
42
+ if (gate) return { outcome: "blocked", reasonCode: gate[1]?.toUpperCase() === "CONTINUING"
43
+ ? "completion-gate-continuing" : "completion-gate-not-approved" };
44
+ if (stopReason === "stop") return hasVisibleText(redactSensitiveText(clean(messageText(message))).text)
45
+ ? { outcome: "completed", reasonCode: null }
46
+ : { outcome: "unknown", reasonCode: "assistant-message-empty" };
47
+ if (["toolUse", "pending", "deferred"].includes(stopReason)) return { outcome: "unknown", reasonCode: "assistant-message-intermediate" };
48
+ return { outcome: "unknown", reasonCode: "operation-settlement-unknown" };
49
+ }
50
+
51
+ function containsRuntimeDrift(value: unknown, depth = 0, remaining = { value: 16_384 }): boolean {
52
+ if (depth > 5 || remaining.value <= 0 || value === null || value === undefined) return false;
53
+ if (typeof value === "string") {
54
+ const text = value.slice(0, remaining.value); remaining.value -= text.length;
55
+ return RUNTIME_DRIFT.test(text);
56
+ }
57
+ if (Array.isArray(value)) return value.some((item) => containsRuntimeDrift(item, depth + 1, remaining));
58
+ if (typeof value !== "object") return false;
59
+ const record = value as Record<string, unknown>;
60
+ return ["reason", "message", "text", "content", "result", "details"]
61
+ .some((key) => containsRuntimeDrift(record[key], depth + 1, remaining));
62
+ }
63
+
64
+ export function runtimeRestartReasonCode(event: unknown): "runtime-restart-required" | null {
65
+ if (!event || typeof event !== "object" || (event as Record<string, unknown>).isError !== true) return null;
66
+ return containsRuntimeDrift((event as Record<string, unknown>).result) ? "runtime-restart-required" : null;
67
+ }
22
68
 
23
69
  export class GatewaySessionStream {
24
70
  readonly sessionRef: string;
@@ -29,57 +75,91 @@ export class GatewaySessionStream {
29
75
  #eventCount = 0;
30
76
  #buffer = "";
31
77
  #truncated = false;
32
- #started = false;
33
78
  #startListeners: Array<() => void> = [];
34
- #settled = false;
79
+ #runtimeRestartRequired = false;
80
+ #lastMessageRef: string | null = null;
81
+ #settlement: Settlement = { outcome: "unknown", reasonCode: "operation-settlement-unknown" };
82
+ #forcedSettlement: Settlement | null = null;
83
+ #lifecycleTerminationReasonCode: string | null = null;
35
84
  #settleListeners: Array<() => void> = [];
36
85
  readonly #toolRefs = new Map<string, string>();
86
+ readonly #lifecycle: SessionOperationLifecycle;
37
87
 
38
- constructor(options: { sessionRef: string; operationRef: string; events: GatewayEventStore }) {
88
+ get runtimeRestartRequired(): boolean { return this.#runtimeRestartRequired; }
89
+ get lifecycleTerminationReasonCode(): string | null { return this.#lifecycleTerminationReasonCode; }
90
+ get operationPhase(): SessionOperationPhase { return this.#lifecycle.phase; }
91
+ get retryAbortRequired(): boolean { return this.#lifecycle.retryAbortRequired; }
92
+
93
+ markAborted(reasonCode = "operation-aborted"): void { this.#forcedSettlement = { outcome: "aborted", reasonCode }; }
94
+ markError(reasonCode = "operation-failed"): void {
95
+ if (!this.#forcedSettlement) this.#forcedSettlement = { outcome: "error", reasonCode };
96
+ }
97
+ forceLifecycleTermination(reasonCode: string): void {
98
+ this.#lifecycleTerminationReasonCode ??= reasonCode;
99
+ const wasStarted = this.#lifecycle.started, wasSettled = this.#lifecycle.hostSettled;
100
+ this.#lifecycle.forceHostSettlement();
101
+ if (!wasStarted) for (const resolve of this.#startListeners.splice(0)) resolve();
102
+ if (!wasSettled) for (const resolve of this.#settleListeners.splice(0)) resolve();
103
+ }
104
+
105
+ constructor(options: { sessionRef: string; operationRef: string; events: GatewayEventStore;
106
+ retryPolicy?: SessionOperationRetryPolicyOptions }) {
39
107
  this.sessionRef = options.sessionRef; this.operationRef = options.operationRef; this.#events = options.events;
108
+ this.#lifecycle = new SessionOperationLifecycle({ operationRef: options.operationRef, retryPolicy: options.retryPolicy });
40
109
  }
41
110
 
42
111
  started(): Promise<void> {
43
- if (this.#started) return Promise.resolve();
112
+ if (this.#lifecycle.started) return Promise.resolve();
44
113
  return new Promise((resolve) => this.#startListeners.push(resolve));
45
114
  }
46
115
 
47
116
  settled(): Promise<void> {
48
- if (this.#settled) return Promise.resolve();
117
+ if (this.#lifecycle.hostSettled) return Promise.resolve();
49
118
  return new Promise((resolve) => this.#settleListeners.push(resolve));
50
119
  }
51
120
 
52
- observe(event: any): void {
121
+ observe(event: any): SessionOperationObservation {
122
+ const wasStarted = this.#lifecycle.started, wasSettled = this.#lifecycle.hostSettled;
123
+ const observation = this.#lifecycle.observe(event);
124
+ if (!observation.accepted) return observation;
125
+ if (!wasStarted && this.#lifecycle.started) for (const resolve of this.#startListeners.splice(0)) resolve();
126
+ if (!wasSettled && this.#lifecycle.hostSettled) for (const resolve of this.#settleListeners.splice(0)) resolve();
127
+ if (observation.retry === "abort") this.markError(observation.reasonCode ?? "automatic-retry-blocked");
53
128
  if (event?.type === "agent_start") {
54
- this.#started = true; for (const resolve of this.#startListeners.splice(0)) resolve(); return;
129
+ return observation;
55
130
  }
56
131
  if (event?.type === "agent_settled") {
57
- this.#settled = true; for (const resolve of this.#settleListeners.splice(0)) resolve(); return;
132
+ return observation;
58
133
  }
59
134
  if (event?.type === "message_start" && event.message?.role === "assistant") {
60
- this.#messageRef = ref("message"); this.#buffer = ""; return;
135
+ this.#messageRef = ref("message"); this.#buffer = ""; return observation;
61
136
  }
62
137
  if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta"
63
138
  && typeof event.assistantMessageEvent.delta === "string") {
64
139
  const remaining = MAX_BUFFER - this.#buffer.length;
65
- if (remaining <= 0) { this.#truncated = true; return; }
140
+ if (remaining <= 0) { this.#truncated = true; return observation; }
66
141
  this.#buffer += event.assistantMessageEvent.delta.slice(0, remaining);
67
142
  if (event.assistantMessageEvent.delta.length > remaining) this.#truncated = true;
68
- this.#flush(false); return;
143
+ this.#flush(false); return observation;
144
+ }
145
+ if (event?.type === "message_end" && event.message?.role === "assistant") {
146
+ this.#flush(true); this.#lastMessageRef = this.#messageRef; this.#settlement = assistantSettlement(event.message); return observation;
69
147
  }
70
- if (event?.type === "message_end" && event.message?.role === "assistant") { this.#flush(true); return; }
71
148
  if (event?.type === "tool_execution_start" && typeof event.toolCallId === "string") {
72
149
  const toolCallRef = ref("tool"); this.#toolRefs.set(event.toolCallId, toolCallRef);
73
150
  this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, toolCallRef,
74
- toolLabel: safeTool(event.toolName), isError: null });
75
- return;
151
+ toolLabel: safeTool(event.toolName), isError: null, reasonCode: null });
152
+ return observation;
76
153
  }
77
154
  if (event?.type === "tool_execution_end" && typeof event.toolCallId === "string") {
78
- const toolCallRef = this.#toolRefs.get(event.toolCallId); if (!toolCallRef) return;
155
+ const toolCallRef = this.#toolRefs.get(event.toolCallId); if (!toolCallRef) return observation;
156
+ const reasonCode = runtimeRestartReasonCode(event);
157
+ if (reasonCode) this.#runtimeRestartRequired = true;
79
158
  this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, toolCallRef,
80
- toolLabel: safeTool(event.toolName), isError: event.isError === true });
159
+ toolLabel: safeTool(event.toolName), isError: event.isError === true, reasonCode });
81
160
  this.#toolRefs.delete(event.toolCallId);
82
161
  }
162
+ return observation;
83
163
  }
84
164
 
85
165
  #flush(final: boolean): void {
@@ -112,9 +192,31 @@ export class GatewaySessionStream {
112
192
  }
113
193
  }
114
194
 
115
- complete(sessionRevision: string): void {
195
+ complete(sessionRevision: string | null, taskOutcome: string | null = null): void {
196
+ if (!this.#lifecycle.markTerminal()) return;
116
197
  this.#flush(true);
117
- if (this.#messageRef) this.#events.publish("message.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef,
118
- messageRef: this.#messageRef, sessionRevision, truncated: this.#truncated });
198
+ let settlement = this.#forcedSettlement ?? (this.#runtimeRestartRequired
199
+ ? { outcome: "unknown" as const, reasonCode: "runtime-restart-required" }
200
+ : this.#settlement);
201
+ const messageRef = this.#lastMessageRef ?? this.#messageRef;
202
+ if (settlement.outcome === "completed" && !messageRef) {
203
+ settlement = { outcome: "unknown", reasonCode: "assistant-message-unavailable" };
204
+ } else if (settlement.outcome === "completed" && taskOutcome === "pending") {
205
+ // Provider completion is not governed-task completion. The durable task
206
+ // contract is authoritative, so withhold both success settlement and the
207
+ // durable assistant-success event until that contract becomes terminal.
208
+ settlement = { outcome: "unknown", reasonCode: "task-completion-pending" };
209
+ } else if (settlement.outcome === "completed" && sessionRevision === null) {
210
+ // Streaming text is not durable success until the canonical session
211
+ // projection confirms it. Still terminate the UI, but do not show draft.
212
+ settlement = { outcome: "unknown", reasonCode: "session-projection-unavailable" };
213
+ }
214
+ if (settlement.outcome === "completed" && messageRef && sessionRevision) {
215
+ this.#events.publish("message.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef,
216
+ messageRef, sessionRevision, truncated: this.#truncated });
217
+ }
218
+ this.#events.publish("operation.settled", { sessionRef: this.sessionRef, operationRef: this.operationRef,
219
+ messageRef, sessionRevision, settlement: settlement.outcome,
220
+ reasonCode: settlement.outcome === "completed" ? null : settlement.reasonCode ?? "operation-settlement-unknown" });
119
221
  }
120
222
  }
@@ -154,8 +154,18 @@ export class SessionCommandController {
154
154
  | "renamed" | "pinned" | "unpinned" | "archived" | "unarchived" | "forked" | "no-change",
155
155
  operationRef: string | null = null;
156
156
  if (command.action === "session.create") {
157
- targetSessionRef = await this.#runtimes.create(String(command.payload.projectRef), String(command.payload.placeRef),
158
- command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
157
+ const createWithReadback = (this.#runtimes as any).createWithReadback;
158
+ if (typeof createWithReadback === "function") {
159
+ const created = await createWithReadback.call(this.#runtimes, String(command.payload.projectRef), String(command.payload.placeRef),
160
+ command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
161
+ targetSessionRef = String(created.sessionRef);
162
+ if (created.effectiveOptions?.state !== "confirmed") throw new Error(
163
+ typeof created.effectiveOptions?.reasonCode === "string" ? created.effectiveOptions.reasonCode : "session-create-options-effect-unknown");
164
+ } else {
165
+ // Compatibility with older embedded supervisors and test doubles.
166
+ targetSessionRef = await this.#runtimes.create(String(command.payload.projectRef), String(command.payload.placeRef),
167
+ command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
168
+ }
159
169
  if (command.payload.permissionMode !== undefined) {
160
170
  await this.#runtimes.setPermission(targetSessionRef, String(command.payload.permissionMode) as
161
171
  "read-only" | "workspace-write" | "trusted-full-access");
@@ -246,9 +256,14 @@ export class SessionCommandController {
246
256
  const code = /recovery/.test(message) ? "recovery-required"
247
257
  : /(owner-conflict|operation-conflict|runtime-busy)/.test(message) ? "owner-conflict"
248
258
  : noDispatchEffect || command.action === "session.send" && !sendAttempted ? "unavailable" : "effect-unknown";
249
- const receipt = code === "effect-unknown" ? this.#uncertain(command, after, afterRow, "session-command-effect-unknown")
259
+ const specificCreateUncertainty = command.action === "session.create" && /^[a-z0-9][a-z0-9.-]{0,95}$/.test(message)
260
+ ? message : "session-command-effect-unknown";
261
+ const receipt = code === "effect-unknown" ? this.#uncertain(command, after, afterRow, specificCreateUncertainty)
250
262
  : this.#rejected(command, after, afterRow, code, message);
251
263
  try { this.#store.settle(command, receipt, this.#now()); } catch { /* intent remains uncertain */ }
264
+ if (command.action === "session.create" && afterRow) {
265
+ this.#events.publish("session.changed", { catalogRevision: after.catalogRevision, session: afterRow });
266
+ }
252
267
  return receipt;
253
268
  }
254
269
  }
@@ -0,0 +1,47 @@
1
+ import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
2
+
3
+ export type EffectiveSessionOptions = {
4
+ state: "confirmed" | "mismatch" | "unknown";
5
+ modelRef: string | null;
6
+ thinkingLevel: string | null;
7
+ reasonCode: string | null;
8
+ };
9
+
10
+ const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
11
+
12
+ export function effectiveModelRef(session: any): string | null {
13
+ const model = session?.model;
14
+ if (!model || typeof model !== "object") return null;
15
+ const provider = String(model.provider ?? ""), id = String(model.id ?? model.modelId ?? "");
16
+ return provider && id ? webUiModelRef(provider, id) : null;
17
+ }
18
+
19
+ export function effectiveThinkingLevel(session: any): string | null {
20
+ let value: unknown;
21
+ try { value = typeof session?.getThinkingLevel === "function" ? session.getThinkingLevel() : undefined; }
22
+ catch { return null; }
23
+ if (value === undefined || value === null || value === "") value = session?.thinkingLevel;
24
+ const level = String(value ?? "");
25
+ return THINKING_LEVELS.has(level) ? level : null;
26
+ }
27
+
28
+ export async function configureSessionOptions(session: any, modelRef: string | null,
29
+ thinkingLevel: string): Promise<EffectiveSessionOptions> {
30
+ if (modelRef) {
31
+ const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
32
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""),
33
+ String(value.id ?? value.modelId ?? "")) === modelRef);
34
+ try { if (model) await session.setModel(model); } catch { /* Canonical read-back below decides the effect. */ }
35
+ }
36
+ try { await session.setThinkingLevel(thinkingLevel); } catch { /* Canonical read-back below decides the effect. */ }
37
+ const effectiveModel = effectiveModelRef(session), effectiveThinking = effectiveThinkingLevel(session);
38
+ if (!effectiveModel) return { state: "unknown", modelRef: null,
39
+ thinkingLevel: effectiveThinking, reasonCode: "session-model-effect-unknown" };
40
+ if (!effectiveThinking) return { state: "unknown", modelRef: effectiveModel,
41
+ thinkingLevel: null, reasonCode: "session-thinking-effect-unknown" };
42
+ if (modelRef && effectiveModel !== modelRef) return { state: "mismatch", modelRef: effectiveModel,
43
+ thinkingLevel: effectiveThinking, reasonCode: "session-model-mismatch" };
44
+ if (effectiveThinking !== thinkingLevel) return { state: "mismatch", modelRef: effectiveModel,
45
+ thinkingLevel: effectiveThinking, reasonCode: "session-thinking-mismatch" };
46
+ return { state: "confirmed", modelRef: effectiveModel, thinkingLevel: effectiveThinking, reasonCode: null };
47
+ }
@@ -0,0 +1,21 @@
1
+ import type { PiagentWebUICanonicalVolatileSessionOperationStateV1 } from "../contracts/generated/session-live-state-v1.ts";
2
+ import type { GatewayTerminalSettlement } from "./gateway-events.ts";
3
+ import type { CurrentOperationProjection } from "./session-runtime-supervisor.ts";
4
+
5
+ export function buildSessionLiveState(options: { gatewayInstanceRef: string; eventSequence: number;
6
+ operations: Array<CurrentOperationProjection & { sessionRef: string }>; settlements: GatewayTerminalSettlement[];
7
+ now?: Date }): PiagentWebUICanonicalVolatileSessionOperationStateV1 {
8
+ const now = options.now ?? new Date();
9
+ if (!Number.isFinite(now.getTime())) throw new Error("session-live-state-time-invalid");
10
+ return {
11
+ schemaVersion: 1,
12
+ version: "piagent-session-live-state-v1",
13
+ generatedAt: now.toISOString(),
14
+ gatewayInstanceRef: options.gatewayInstanceRef,
15
+ eventSequence: options.eventSequence,
16
+ state: "ready",
17
+ operations: [...options.operations].sort((left, right) => left.sessionRef.localeCompare(right.sessionRef)),
18
+ settlements: [...options.settlements].sort((left, right) => right.sequence - left.sequence).slice(0, 100),
19
+ reasonCode: null
20
+ };
21
+ }