@piagent/platform 1.5.5 → 1.6.0

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 (227) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +9 -8
  3. package/README.vi.md +9 -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 +68 -0
  24. package/benchmarks/production-v1/suite.json +42 -3
  25. package/catalog/capabilities.json +6 -6
  26. package/docs/command-reference-vietnamese.md +6 -6
  27. package/docs/context-engine.md +7 -1
  28. package/docs/distribution-standard.md +7 -7
  29. package/docs/en/architecture.md +1 -0
  30. package/docs/languages.json +4 -4
  31. package/docs/model-options.md +8 -5
  32. package/docs/operator-manual-vietnamese.md +13 -11
  33. package/docs/project-adapters.md +2 -2
  34. package/docs/publishing-for-teams.md +5 -0
  35. package/docs/quality-benchmark.md +271 -62
  36. package/docs/release-install-policy.md +10 -9
  37. package/docs/runtime-policy-design.md +4 -4
  38. package/docs/runtime-quality-baseline.md +13 -6
  39. package/docs/security-threat-model.md +2 -2
  40. package/docs/task-lifecycle-vietnamese.md +2 -2
  41. package/docs/team-onboarding.md +2 -2
  42. package/docs/usage-observability.md +7 -2
  43. package/docs/vi/architecture.md +1 -0
  44. package/docs-site/architecture.html +1 -1
  45. package/docs-site/benchmark.html +1 -1
  46. package/docs-site/commands.html +1 -1
  47. package/docs-site/context-engine.html +1 -1
  48. package/docs-site/en/architecture.html +1 -1
  49. package/docs-site/en/benchmark.html +1 -1
  50. package/docs-site/en/commands.html +1 -1
  51. package/docs-site/en/context-engine.html +1 -1
  52. package/docs-site/en/index.html +1 -1
  53. package/docs-site/en/mcp-approval.html +1 -1
  54. package/docs-site/en/mcp-auth.html +1 -1
  55. package/docs-site/en/mcp-servers.html +1 -1
  56. package/docs-site/en/mcp.html +1 -1
  57. package/docs-site/en/permissions.html +1 -1
  58. package/docs-site/en/profiles.html +1 -1
  59. package/docs-site/en/quickstart.html +1 -1
  60. package/docs-site/en/runtime.html +1 -1
  61. package/docs-site/en/scope.html +1 -1
  62. package/docs-site/en/security.html +1 -1
  63. package/docs-site/en/team.html +1 -1
  64. package/docs-site/en/whats-new.html +1 -1
  65. package/docs-site/en/workflows.html +1 -1
  66. package/docs-site/index.html +1 -1
  67. package/docs-site/mcp-approval.html +1 -1
  68. package/docs-site/mcp-auth.html +1 -1
  69. package/docs-site/mcp-servers.html +1 -1
  70. package/docs-site/mcp.html +1 -1
  71. package/docs-site/permissions.html +1 -1
  72. package/docs-site/profiles.html +1 -1
  73. package/docs-site/quickstart.html +1 -1
  74. package/docs-site/runtime.html +1 -1
  75. package/docs-site/scope.html +1 -1
  76. package/docs-site/security.html +1 -1
  77. package/docs-site/team.html +1 -1
  78. package/docs-site/whats-new.html +1 -1
  79. package/docs-site/workflows.html +1 -1
  80. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  81. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  82. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  83. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  84. package/evals/fs4-readiness-matrix.v1.json +2 -2
  85. package/evals/fs5-causal-arm.v1.json +2 -2
  86. package/evals/fs5-pilot-protocol.v1.json +2 -2
  87. package/evals/fs5-pilot-protocol.v4.json +6 -5
  88. package/evals/fs5-pilot-protocol.v5.json +7 -6
  89. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  90. package/package.json +3 -1
  91. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  92. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  93. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  94. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  95. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  96. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  97. package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
  98. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  99. package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
  100. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  101. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  102. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  103. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  104. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  105. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  106. package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
  107. package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
  108. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  109. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  110. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
  111. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  112. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  113. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  114. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  115. package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
  116. package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
  117. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  118. package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
  119. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  120. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  121. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  122. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  123. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  124. package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
  125. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  126. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  127. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  128. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  129. package/packages/piagent-core/extensions/context-engine.js +148 -145
  130. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  131. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  132. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  133. package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
  134. package/packages/piagent-core/extensions/criterion-graph.js +78 -8
  135. package/packages/piagent-core/extensions/guard-types.ts +2 -0
  136. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  137. package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
  138. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  139. package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
  140. package/packages/piagent-core/extensions/task-state.js +40 -134
  141. package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
  142. package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
  143. package/packages/piagent-core/package.json +1 -1
  144. package/packages/piagent-core/policies/base-policy.json +2 -0
  145. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  146. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  147. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  148. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  149. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  150. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  151. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  152. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  153. package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
  154. package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
  155. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  156. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
  157. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  158. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
  159. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  160. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  161. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  162. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  163. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
  164. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
  165. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  166. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  167. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  168. package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
  169. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  170. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
  171. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
  172. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  173. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
  174. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  175. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  176. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
  177. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  178. package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
  179. package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
  180. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
  181. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
  182. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  183. package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
  184. package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
  185. package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
  186. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  187. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  188. package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
  189. package/packages/piagent-core/security/sensitive-data.js +13 -218
  190. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  191. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  192. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  193. package/packages/piagent-core/security/sensitive-text.js +234 -0
  194. package/packages/piagent-core/subagents/piagent-worker.md +1 -1
  195. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
  196. package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
  197. package/packages/piagent-webui/dist/client/index.html +1 -1
  198. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  199. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  200. package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
  201. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  202. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  203. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  204. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  205. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
  206. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  207. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
  208. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  209. package/packages/piagent-webui/server/transcript-projection.ts +22 -4
  210. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  211. package/schemas/piagent-webui/catalog-v1.json +9 -0
  212. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  213. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  214. package/schemas/task-contract.schema.json +12 -1
  215. package/scripts/benchmark-runner-core.mjs +355 -359
  216. package/scripts/benchmark-runner-finalization.mjs +309 -0
  217. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  218. package/scripts/benchmark-runner-support.mjs +200 -0
  219. package/scripts/benchmark-session.mjs +95 -41
  220. package/scripts/configure-model-scope.sh +4 -3
  221. package/scripts/helper-evaluation.mjs +1 -1
  222. package/scripts/install-global.sh +2 -2
  223. package/scripts/pi-usage-history.mjs +37 -10
  224. package/scripts/setup.sh +2 -2
  225. package/scripts/verify-local.sh +5 -1
  226. package/templates/global/settings.json +2 -1
  227. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+
3
+ import type { PiSessionInfo } from "./session-catalog.ts";
4
+ import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
5
+ import { rpcUiContext } from "./rpc-ui-context.ts";
6
+
7
+ export type RuntimeHandle = { dispose(): Promise<void>; session?: any };
8
+ export type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string,
9
+ sessionManager?: any) => Promise<RuntimeHandle>;
10
+
11
+ export function createProductionRuntimeFactory(options: {
12
+ host: any;
13
+ agentDir: string;
14
+ packageRoot: string;
15
+ modelRuntime?: any;
16
+ }): RuntimeFactory {
17
+ return async (info, _runtimeInstanceRef, sessionManager) => {
18
+ const guard = path.join(options.packageRoot, "packages", "piagent-core", "extensions", "piagent-guard.ts");
19
+ const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
20
+ const services = await options.host.createAgentSessionServices({
21
+ cwd, agentDir, modelRuntime: options.modelRuntime,
22
+ resourceLoaderOptions: {
23
+ additionalExtensionPaths: [guard],
24
+ extensionsOverride: preferAuthoritativePiagentGuard(guard)
25
+ }
26
+ });
27
+ const extensionErrors = services.resourceLoader.getExtensions().errors;
28
+ if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
29
+ const created = await options.host.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent });
30
+ return { ...created, services, diagnostics: services.diagnostics };
31
+ };
32
+ const manager = sessionManager ?? options.host.SessionManager.open(info.path);
33
+ const runtime = await options.host.createAgentSessionRuntime(createRuntime, {
34
+ cwd: info.cwd,
35
+ agentDir: options.agentDir,
36
+ sessionManager: manager,
37
+ sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile: info.path }
38
+ });
39
+ const bind = async (session: any) => session.bindExtensions({ mode: "rpc", uiContext: rpcUiContext() });
40
+ runtime.setRebindSession(bind);
41
+ try { await bind(runtime.session); }
42
+ catch (error) { await runtime.dispose().catch(() => undefined); throw error; }
43
+ return runtime;
44
+ };
45
+ }
@@ -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,24 @@ 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,
13
+ type EffectiveSessionOptions } from "./session-effective-options.ts";
17
14
  import { waitForOperationStart } from "./session-operation-start.ts";
18
-
15
+ import { armSessionOperationWatchdog, bestEffortUnsubscribe, boundedResult, sessionOperationDeadlinePolicy, SessionOperationWatchdog, terminateWatchedSessionOperation,
16
+ type SessionOperationDeadlinePolicy, type SessionOperationWatchdogOptions } from "./session-operation-watchdog.ts";
17
+ import { createProductionRuntimeFactory, type RuntimeFactory, type RuntimeHandle } from "./session-runtime-factory.ts";
19
18
  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
19
  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 };
20
+ stream: GatewaySessionStream | null; unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
21
+ unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null;
22
+ watchdog: SessionOperationWatchdog | null; lastSessionRevision: string | null };
26
23
  type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
27
24
  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
-
25
+ export type { EffectiveSessionOptions } from "./session-effective-options.ts";
26
+ export type SessionCreateResult = { sessionRef: string; effectiveOptions: EffectiveSessionOptions };
27
+ export type CurrentOperationProjection = { operationRef: string; state: "running" | "waiting-approval" | "settling";
28
+ abortable: boolean };
60
29
  export class SessionRuntimeSupervisor {
61
30
  readonly #gatewayInstanceRef: string;
62
31
  readonly #key: Buffer;
@@ -65,13 +34,13 @@ export class SessionRuntimeSupervisor {
65
34
  readonly #runtimeFactory: RuntimeFactory;
66
35
  readonly #events: GatewayEventStore;
67
36
  readonly #host: any | null;
37
+ readonly #operationDeadlinePolicy: SessionOperationDeadlinePolicy;
68
38
  readonly #resolveProject: ((projectRef: string) => string | null) | null;
69
39
  readonly #active = new Map<string, ActiveRuntime>();
70
40
  readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
71
41
  readonly #created = new Map<string, PiSessionInfo>();
72
42
  #closed = false;
73
43
  #readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
74
-
75
44
  constructor(options: {
76
45
  gatewayInstanceRef: string;
77
46
  key: Buffer;
@@ -83,6 +52,7 @@ export class SessionRuntimeSupervisor {
83
52
  packageRoot?: string;
84
53
  modelRuntime?: any;
85
54
  events?: GatewayEventStore;
55
+ operationWatchdog?: SessionOperationWatchdogOptions;
86
56
  resolveProject?(projectRef: string): string | null;
87
57
  }) {
88
58
  this.#gatewayInstanceRef = options.gatewayInstanceRef;
@@ -91,19 +61,17 @@ export class SessionRuntimeSupervisor {
91
61
  this.#listSessions = options.listSessions;
92
62
  this.#events = options.events ?? new GatewayEventStore();
93
63
  this.#host = options.host ?? null;
64
+ this.#operationDeadlinePolicy = sessionOperationDeadlinePolicy(options.operationWatchdog);
94
65
  this.#resolveProject = options.resolveProject ?? null;
95
66
  if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
96
67
  else {
97
68
  if (!options.host || !options.agentDir || !options.packageRoot) throw new Error("session-runtime-factory-unavailable");
98
- this.#runtimeFactory = productionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
69
+ this.#runtimeFactory = createProductionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
99
70
  packageRoot: options.packageRoot, modelRuntime: options.modelRuntime });
100
71
  }
101
72
  }
102
-
103
73
  get activeCount(): number { return this.#active.size; }
104
-
105
74
  setProjectionReader(reader: (sessionRef: string) => Promise<Projection>): void { this.#readProjection = reader; }
106
-
107
75
  async listSessions(): Promise<PiSessionInfo[]> {
108
76
  const persisted = (await this.#listSessions()).filter(isUserConversationSession);
109
77
  const paths = new Set(persisted.map((info) => info.path));
@@ -112,8 +80,11 @@ export class SessionRuntimeSupervisor {
112
80
  }
113
81
  return [...persisted, ...[...this.#created.values()].filter((info) => !paths.has(info.path))];
114
82
  }
115
-
116
83
  async create(projectRef: string, placeRef: string, modelRef: string | null, thinkingLevel: string): Promise<string> {
84
+ return (await this.createWithReadback(projectRef, placeRef, modelRef, thinkingLevel)).sessionRef;
85
+ }
86
+ async createWithReadback(projectRef: string, placeRef: string, modelRef: string | null,
87
+ thinkingLevel: string): Promise<SessionCreateResult> {
117
88
  if (this.#closed || !this.#host) throw new Error("session-create-unavailable");
118
89
  if (placeRef !== projectRef) throw new Error("session-place-mismatch");
119
90
  const sessions = (await this.#listSessions()).filter(isUserConversationSession);
@@ -128,22 +99,19 @@ export class SessionRuntimeSupervisor {
128
99
  const created: PiSessionInfo = { path: sessionFile, id: manager.getSessionId(), cwd: candidates[0], created: createdAt,
129
100
  modified: createdAt, messageCount: 0, firstMessage: "(no messages)", allMessagesText: "" };
130
101
  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);
102
+ // Retain identity before fallible runtime work so create receipts never point at an orphan.
141
103
  this.#created.set(sessionRef, created);
142
- return sessionRef;
104
+ try { await this.#activate(sessionRef, created, manager); }
105
+ catch {
106
+ return { sessionRef, effectiveOptions: { state: "unknown", modelRef: null, thinkingLevel: null,
107
+ reasonCode: "session-runtime-open-failed" } };
108
+ }
109
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
110
+ if (!session) return { sessionRef, effectiveOptions: { state: "unknown", modelRef: null, thinkingLevel: null,
111
+ reasonCode: "session-runtime-unavailable" } };
112
+ return { sessionRef, effectiveOptions: await configureSessionOptions(session, modelRef, thinkingLevel) };
143
113
  }
144
-
145
114
  liveSessionManager(sessionRef: string): any | null { return this.#active.get(sessionRef)?.sessionManager ?? null; }
146
-
147
115
  async acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
148
116
  if (this.#closed) throw new Error("session-runtime-supervisor-closed");
149
117
  const active = this.#active.get(sessionRef);
@@ -154,7 +122,6 @@ export class SessionRuntimeSupervisor {
154
122
  this.#opening.set(sessionRef, opening);
155
123
  return await opening;
156
124
  }
157
-
158
125
  async #acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
159
126
  if (this.#active.size + this.#opening.size > MAX_WARM_RUNTIMES) throw new Error("session-runtime-capacity");
160
127
  const found = (await this.#listSessions()).filter(isUserConversationSession)
@@ -162,7 +129,6 @@ export class SessionRuntimeSupervisor {
162
129
  if (found.length !== 1) throw new Error(found.length ? "session-ref-ambiguous" : "session-not-found");
163
130
  return await this.#activate(sessionRef, found[0]!);
164
131
  }
165
-
166
132
  async #activate(sessionRef: string, info: PiSessionInfo, sessionManager?: any): Promise<SessionLeaseSnapshot> {
167
133
  if (this.#active.has(sessionRef)) throw new Error("session-owner-conflict");
168
134
  const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
@@ -179,9 +145,9 @@ export class SessionRuntimeSupervisor {
179
145
  await runtime.dispose().catch(() => undefined);
180
146
  throw new Error("session-owner-continuity-lost");
181
147
  }
182
- const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, unsubscribe: null, completion: null,
148
+ const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, stream: null, unsubscribe: null, completion: null,
183
149
  settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null,
184
- sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null };
150
+ sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null, watchdog: null, lastSessionRevision: null };
185
151
  this.#active.set(sessionRef, active);
186
152
  this.#bindApproval(sessionRef, active);
187
153
  return current;
@@ -191,7 +157,6 @@ export class SessionRuntimeSupervisor {
191
157
  throw error;
192
158
  }
193
159
  }
194
-
195
160
  async release(sessionRef: string): Promise<SessionLeaseSnapshot> {
196
161
  const active = this.#active.get(sessionRef);
197
162
  if (!active) {
@@ -203,7 +168,7 @@ export class SessionRuntimeSupervisor {
203
168
  if (active.settling && active.completion) await active.completion;
204
169
  if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
205
170
  this.#active.delete(sessionRef);
206
- active.unsubscribeApproval?.(); active.unbindApproval?.();
171
+ bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
207
172
  try { await active.runtime.dispose(); }
208
173
  catch (error) {
209
174
  try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
@@ -215,7 +180,28 @@ export class SessionRuntimeSupervisor {
215
180
  if (transient && !(await this.#listSessions()).some((info) => info.path === transient.path)) this.#created.delete(sessionRef);
216
181
  return released;
217
182
  }
218
-
183
+ async restart(sessionRef: string): Promise<SessionLeaseSnapshot> {
184
+ const active = this.#active.get(sessionRef);
185
+ if (!active) return await this.acquire(sessionRef);
186
+ if (active.operationRef || active.settling || !active.runtime.session?.isIdle) throw new Error("session-runtime-busy");
187
+ const info = active.info, manager = active.sessionManager; await this.release(sessionRef);
188
+ return await this.#activate(sessionRef, info, manager ?? undefined);
189
+ }
190
+ currentOperation(sessionRef: string): CurrentOperationProjection | null {
191
+ const active = this.#active.get(sessionRef);
192
+ if (!active?.operationRef) return null;
193
+ return { operationRef: active.operationRef,
194
+ state: active.settling ? "settling" : active.approvalWaiting ? "waiting-approval" : "running",
195
+ abortable: !active.settling };
196
+ }
197
+ currentOperations(): Array<CurrentOperationProjection & { sessionRef: string }> {
198
+ const result: Array<CurrentOperationProjection & { sessionRef: string }> = [];
199
+ for (const sessionRef of this.#active.keys()) {
200
+ const operation = this.currentOperation(sessionRef);
201
+ if (operation) result.push({ sessionRef, ...operation });
202
+ }
203
+ return result;
204
+ }
219
205
  ownership(sessionRef: string): SessionOwnerProjection {
220
206
  const lease = this.#leases.inspect(sessionRef);
221
207
  const active = this.#active.get(sessionRef);
@@ -225,7 +211,7 @@ export class SessionRuntimeSupervisor {
225
211
  reasonCode: null
226
212
  };
227
213
  if (lease.state === "gateway-owned" && active && active.lease.ownerEpoch === lease.ownerEpoch
228
- && lease.gatewayInstanceRef === this.#gatewayInstanceRef) return {
214
+ && lease.gatewayInstanceRef === this.#gatewayInstanceRef && active.lease.runtimeInstanceRef === lease.runtimeInstanceRef) return {
229
215
  state: "gateway-owned", liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : "idle",
230
216
  composerAvailable: true, needsAttention: active.approvalWaiting,
231
217
  owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
@@ -250,10 +236,6 @@ export class SessionRuntimeSupervisor {
250
236
  reasonCode: lease.reasonCode ?? "session-owner-continuity-unknown"
251
237
  };
252
238
  }
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
239
  async send(sessionRef: string, payload: { delivery: "new-operation" | "follow-up" | "steer"; message: string;
258
240
  expectedOperationRef: string | null; images?: unknown[] }, sessionRevision: string):
259
241
  Promise<{ resultCode: "started" | "queued" | "steered"; operationRef: string }> {
@@ -261,7 +243,7 @@ export class SessionRuntimeSupervisor {
261
243
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
262
244
  if (!active || !session) throw new Error("session-runtime-unavailable");
263
245
  if (payload.delivery !== "new-operation") {
264
- if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || !session.isStreaming) throw new Error("session-operation-conflict");
246
+ if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || active.settling || !session.isStreaming) throw new Error("session-operation-conflict");
265
247
  const images = payload.images?.length ? payload.images : undefined;
266
248
  if (payload.delivery === "follow-up") { await session.followUp(payload.message, images); return { resultCode: "queued", operationRef: active.operationRef }; }
267
249
  await session.steer(payload.message, images); return { resultCode: "steered", operationRef: active.operationRef };
@@ -269,8 +251,14 @@ export class SessionRuntimeSupervisor {
269
251
  if (payload.expectedOperationRef !== null || active.operationRef || active.settling || !session.isIdle) throw new Error("session-operation-conflict");
270
252
  const operationRef = `operation_${randomBytes(24).toString("base64url")}`;
271
253
  const stream = new GatewaySessionStream({ sessionRef, operationRef, events: this.#events });
272
- active.operationRef = operationRef;
273
- active.unsubscribe = session.subscribe((event: unknown) => stream.observe(event));
254
+ const watchdog = new SessionOperationWatchdog(this.#operationDeadlinePolicy);
255
+ active.operationRef = operationRef; active.stream = stream; active.watchdog = watchdog; active.lastSessionRevision = sessionRevision;
256
+ try {
257
+ active.unsubscribe = armSessionOperationWatchdog({ watchdog, subscribe: (listener) => session.subscribe(listener),
258
+ observe: (event) => stream.observe(event), expire: (reasonCode) => { void this.#terminateOperation(sessionRef,
259
+ operationRef, "error", reasonCode, reasonCode, true).catch(() => undefined); } });
260
+ } catch (error) { stream.markError("session-operation-start-failed");
261
+ await this.#quarantineRuntime(sessionRef, operationRef, active, stream, "session-operation-start-failed"); throw error; }
274
262
  this.#events.publish("runtime.changed", { sessionRef, sessionRevision, liveState: "running", operationRef, reasonCode: null });
275
263
  const created = this.#created.get(sessionRef);
276
264
  if (created && created.firstMessage === "(no messages)") {
@@ -278,35 +266,51 @@ export class SessionRuntimeSupervisor {
278
266
  }
279
267
  let prompt: Promise<void>;
280
268
  try { prompt = session.prompt(payload.message, payload.images?.length ? { images: payload.images } : undefined); }
281
- catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
269
+ catch (error) { stream.markError(); await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
282
270
  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.
271
+ // `agent_settled` is canonical even when a workflow's outer prompt resolves first.
286
272
  const lifecycle = started.then(async (mode) => {
287
273
  if (mode === "deferred") await stream.settled();
288
274
  else await Promise.race([stream.settled(), prompt]);
289
275
  });
290
276
  active.completion = lifecycle.then(() => this.#finishOperation(sessionRef, operationRef, stream),
291
- () => this.#finishOperation(sessionRef, operationRef, stream));
277
+ () => { stream.markError(); return this.#finishOperation(sessionRef, operationRef, stream); });
292
278
  try { await started; }
293
279
  catch (error) { await active.completion; throw error; }
294
280
  return { resultCode: "started", operationRef };
295
281
  }
296
-
297
282
  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;
283
+ await this.#terminateOperation(sessionRef, operationRef, "aborted", "operation-aborted", "operation-abort-timeout", clearQueued);
284
+ }
285
+ async #terminateOperation(sessionRef: string, operationRef: string, settlement: "aborted" | "error",
286
+ reasonCode: string, forcedReasonCode: string, clearQueued: boolean): Promise<void> {
287
+ const active = this.#active.get(sessionRef), session = active?.runtime.session, stream = active?.stream, watchdog = active?.watchdog;
288
+ if (!active || !session || !stream || !watchdog || active.operationRef !== operationRef) throw new Error("session-operation-conflict");
289
+ if (active.settling && !watchdog.terminating) throw new Error("session-operation-conflict");
290
+ active.settling = true; active.approvalWaiting = false;
291
+ const result = await terminateWatchedSessionOperation({ watchdog, settlement, reasonCode, forcedReasonCode, stream,
292
+ completion: () => active.completion,
293
+ settledCleanly: () => this.#active.get(sessionRef) === active && active.operationRef !== operationRef
294
+ && session.isIdle === true && session.isStreaming !== true,
295
+ cancelApproval: (reason) => { piApprovalBroker.cancelForOperation(active.info.cwd, active.info.id, operationRef, reason); },
296
+ abortHost: () => session.abort(), clearQueue: clearQueued ? () => session.clearQueue() : undefined
297
+ });
298
+ if (result.state === "quarantine") await this.#quarantineRuntime(sessionRef, operationRef, active, stream, result.reasonCode);
299
+ else if (this.#active.get(sessionRef) === active) { active.stream = null; active.watchdog = null; active.settling = false; }
300
+ }
301
+ async #quarantineRuntime(sessionRef: string, operationRef: string, active: ActiveRuntime,
302
+ stream: GatewaySessionStream, reasonCode: string): Promise<void> {
303
+ if (this.#active.get(sessionRef) !== active) return;
304
+ bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.watchdog = null;
305
+ if (active.operationRef === operationRef) { active.operationRef = null; stream.complete(null); }
306
+ active.stream = null; active.completion = null; active.settling = false; active.approvalWaiting = false;
307
+ this.#active.delete(sessionRef); bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
308
+ try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
309
+ active.lease.runtimeInstanceRef!, reasonCode); } catch { /* continuity remains fail closed */ }
310
+ if (active.lastSessionRevision) this.#events.publish("runtime.changed", { sessionRef,
311
+ sessionRevision: active.lastSessionRevision, liveState: "uncertain", operationRef: null, reasonCode });
312
+ await boundedResult(Promise.resolve().then(() => active.runtime.dispose()), this.#operationDeadlinePolicy.terminationTimeoutMs);
308
313
  }
309
-
310
314
  #bindApproval(sessionRef: string, active: ActiveRuntime): void {
311
315
  const authority = (): ApprovalAuthority => {
312
316
  if (!active.operationRef || active.lease.state !== "gateway-owned" || !active.lease.revision) return null;
@@ -331,50 +335,53 @@ export class SessionRuntimeSupervisor {
331
335
  active.unbindApproval = piApprovalBroker.bind({ cwd: active.info.cwd, rawSessionId: active.info.id,
332
336
  runtimeInstanceId: active.lease.runtimeInstanceRef!, authority });
333
337
  active.unsubscribeApproval = piApprovalBroker.subscribe(active.info.cwd, active.info.id, (event: ApprovalBrokerEvent) => {
338
+ active.watchdog?.progress();
334
339
  const projection = piApprovalBroker.projection(active.info.cwd, active.info.id);
335
340
  active.approvalWaiting = projection.summary.state === "waiting";
336
341
  void this.#publishApprovalState(sessionRef, active, event);
337
342
  });
338
343
  }
339
-
340
344
  async #publishApprovalState(sessionRef: string, active: ActiveRuntime, event: ApprovalBrokerEvent): Promise<void> {
341
345
  try {
342
346
  const projection = await this.#readProjection?.(sessionRef);
343
347
  if (!projection || this.#active.get(sessionRef) !== active) return;
348
+ active.lastSessionRevision = projection.sessionRevision;
344
349
  this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
345
350
  liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : projection.liveState,
346
351
  operationRef: active.operationRef, reasonCode: `approval-${event.kind}` });
347
352
  } catch { /* approval truth remains available through the canonical inspection route */ }
348
353
  }
349
-
350
354
  async setModel(sessionRef: string, modelRef: string): Promise<"model-changed" | "no-change"> {
351
355
  await this.acquire(sessionRef);
352
356
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
353
357
  if (!active || !session) throw new Error("session-runtime-unavailable");
354
358
  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;
359
+ const current = effectiveModelRef(session);
357
360
  if (current === modelRef) return "no-change";
358
361
  const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
359
- const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
362
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? value.modelId ?? "")) === modelRef);
360
363
  if (!model) throw new Error("session-model-unavailable");
361
364
  await session.setModel(model);
365
+ const effective = effectiveModelRef(session);
366
+ if (!effective) throw new Error("session-model-effect-unknown");
367
+ if (effective !== modelRef) throw new Error("session-model-mismatch");
362
368
  this.#touchCreated(sessionRef);
363
369
  return "model-changed";
364
370
  }
365
-
366
371
  async setThinking(sessionRef: string, thinkingLevel: string): Promise<"thinking-changed" | "no-change"> {
367
372
  await this.acquire(sessionRef);
368
373
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
369
374
  if (!active || !session) throw new Error("session-runtime-unavailable");
370
375
  if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
371
- const current = String(session.thinkingLevel ?? session.getThinkingLevel?.() ?? "unknown");
376
+ const current = effectiveThinkingLevel(session);
372
377
  if (current === thinkingLevel) return "no-change";
373
- session.setThinkingLevel(thinkingLevel);
378
+ await session.setThinkingLevel(thinkingLevel);
379
+ const effective = effectiveThinkingLevel(session);
380
+ if (!effective) throw new Error("session-thinking-effect-unknown");
381
+ if (effective !== thinkingLevel) throw new Error("session-thinking-mismatch");
374
382
  this.#touchCreated(sessionRef);
375
383
  return "thinking-changed";
376
384
  }
377
-
378
385
  async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
379
386
  await this.acquire(sessionRef);
380
387
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
@@ -384,7 +391,6 @@ export class SessionRuntimeSupervisor {
384
391
  this.#touchCreated(sessionRef);
385
392
  return "permission-changed";
386
393
  }
387
-
388
394
  async runRuntimeCommand(sessionRef: string, command: string): Promise<RuntimeCommandResult> {
389
395
  await this.acquire(sessionRef);
390
396
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
@@ -398,12 +404,10 @@ export class SessionRuntimeSupervisor {
398
404
  health: { state: "unavailable", reasonCode: "approval-broker-unavailable", message: "Approval broker is unavailable" } } };
399
405
  return piApprovalBroker.projection(active.info.cwd, active.info.id);
400
406
  }
401
-
402
407
  approvalDetail(sessionRef: string, approvalRef: string): unknown | null {
403
408
  const active = this.#active.get(sessionRef);
404
409
  return active ? piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef) : null;
405
410
  }
406
-
407
411
  async decideApproval(approvalRef: string, decision: unknown): Promise<unknown> {
408
412
  for (const active of this.#active.values()) {
409
413
  if (piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef)) {
@@ -412,7 +416,6 @@ export class SessionRuntimeSupervisor {
412
416
  }
413
417
  throw new Error("approval-not-pending");
414
418
  }
415
-
416
419
  async rename(sessionRef: string, title: string): Promise<"renamed" | "no-change"> {
417
420
  await this.acquire(sessionRef);
418
421
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
@@ -424,7 +427,6 @@ export class SessionRuntimeSupervisor {
424
427
  this.#touchCreated(sessionRef);
425
428
  return "renamed";
426
429
  }
427
-
428
430
  async fork(sessionRef: string, entryRef: string | null, title: string | null): Promise<string> {
429
431
  if (!this.#host) throw new Error("session-fork-unavailable");
430
432
  await this.acquire(sessionRef);
@@ -451,28 +453,41 @@ export class SessionRuntimeSupervisor {
451
453
  this.#created.set(forkRef, created);
452
454
  return forkRef;
453
455
  }
454
-
455
- #touchCreated(sessionRef: string): void {
456
- const created = this.#created.get(sessionRef);
457
- if (created) created.modified = new Date();
458
- }
459
-
456
+ #touchCreated(sessionRef: string): void { const created = this.#created.get(sessionRef); if (created) created.modified = new Date(); }
460
457
  async #finishOperation(sessionRef: string, operationRef: string, stream: GatewaySessionStream): Promise<void> {
461
458
  const active = this.#active.get(sessionRef);
462
459
  if (!active || active.operationRef !== operationRef) return;
463
- active.unsubscribe?.(); active.unsubscribe = null; active.operationRef = null; active.settling = true;
460
+ const restartRequired = stream.runtimeRestartRequired; let projection: Projection | null = null;
461
+ bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.settling = true;
464
462
  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 });
463
+ if (this.#readProjection) {
464
+ const read = await boundedResult(this.#readProjection(sessionRef), this.#operationDeadlinePolicy.projectionTimeoutMs);
465
+ if (read.state === "settled") projection = read.value;
466
+ else if (read.state === "timeout") stream.markError("session-projection-timeout");
467
+ }
470
468
  } 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; }
469
+ if (this.#active.get(sessionRef) !== active || active.operationRef !== operationRef) return;
470
+ 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; }
471
+ active.operationRef = null;
472
+ if (projection) active.lastSessionRevision = projection.sessionRevision;
473
+ stream.complete(projection?.sessionRevision ?? null);
474
+ if (projection) this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
475
+ liveState: restartRequired ? "uncertain" : projection.liveState, operationRef: null,
476
+ reasonCode: restartRequired ? "runtime-restart-required" : null });
477
+ if (this.#active.get(sessionRef) === active) { active.completion = null;
478
+ if (!active.watchdog?.terminating) { active.stream = null; active.watchdog = null; active.settling = false; } }
479
+ if (!restartRequired || !projection || this.#active.get(sessionRef) !== active) return;
480
+ let restartAuthority: ActiveRuntime | null = active;
481
+ try {
482
+ await this.restart(sessionRef); restartAuthority = this.#active.get(sessionRef) ?? null;
483
+ const projection = await this.#readProjection?.(sessionRef);
484
+ if (!projection || !restartAuthority || this.#active.get(sessionRef) !== restartAuthority || this.ownership(sessionRef).state !== "gateway-owned") return;
485
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision, liveState: projection.liveState, operationRef: null, reasonCode: null });
486
+ } catch {
487
+ if (!restartAuthority || this.#active.get(sessionRef) !== restartAuthority || this.ownership(sessionRef).state !== "gateway-owned") return;
488
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision, liveState: "uncertain", operationRef: null, reasonCode: "runtime-restart-failed" });
473
489
  }
474
490
  }
475
-
476
491
  async close(): Promise<void> {
477
492
  if (this.#closed) return;
478
493
  this.#closed = true;
@@ -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");
@@ -1,11 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
4
+ import { hasVisibleText } from "../shared/text-visibility.ts";
4
5
 
5
6
  const MAX_ENTRIES = 50_000;
6
7
  const MAX_ITEMS = 200;
7
8
  const MAX_TEXT = 16_384;
8
- const ANSI = /\u001b(?:[@-_]|\[[0-?]*[ -/]*[@-~])/g;
9
+ const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g;
10
+ const COMPLETION_GATE = /^\[Piagent completion gate: (CONTINUING|NOT APPROVED)\]/i;
9
11
 
10
12
  type TranscriptIdentity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string | null; taskRunId: string | null;
11
13
  agentOperationId: null; toolCallId: null };
@@ -87,6 +89,21 @@ function assistantFailureReason(message: any): string | null {
87
89
  if (/(?:network|fetch failed|econn|timed? ?out|unavailable)/.test(detail)) return "provider-unavailable";
88
90
  return "provider-response-failed";
89
91
  }
92
+ function assistantUnavailableReason(message: any, text: string, projectedToolCalls: TranscriptItem["toolCalls"]): string | null {
93
+ if (message?.role !== "assistant") return null;
94
+ const stopReason = String(message?.stopReason ?? "");
95
+ if (stopReason === "error") return assistantFailureReason(message) ?? "provider-response-failed";
96
+ if (stopReason === "aborted") return "assistant-message-aborted";
97
+ if (stopReason === "length") return "assistant-output-incomplete";
98
+ const gate = COMPLETION_GATE.exec(text.trimStart());
99
+ if (gate) return gate[1]?.toUpperCase() === "CONTINUING"
100
+ ? "assistant-completion-continuing" : "assistant-completion-not-approved";
101
+ if (projectedToolCalls.length > 0 || ["toolUse", "pending", "deferred"].includes(stopReason)) return "assistant-intermediate-output";
102
+ if (!stopReason) return "assistant-settlement-unknown";
103
+ if (stopReason && stopReason !== "stop") return "assistant-settlement-unknown";
104
+ if (!hasVisibleText(text)) return "assistant-message-empty";
105
+ return null;
106
+ }
90
107
  function safeText(value: string): { full: string; preview: string; redacted: boolean; truncated: boolean } {
91
108
  const clean = value.replace(ANSI, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
92
109
  const redaction = redactSensitiveText(clean);
@@ -129,17 +146,18 @@ function item(entry: any, identity: TranscriptIdentity): TranscriptItem | null {
129
146
  const itemRole = role(entry.message), recordedAt = timestamp(entry.timestamp ?? entry.message.timestamp);
130
147
  if (!itemRole || !recordedAt || typeof entry.id !== "string" || entry.id.length === 0) return null;
131
148
  const userProjection = userMessageProjection(entry.message);
149
+ const projectedToolCalls = toolCalls(entry.message, identity.sessionRef, itemRole);
132
150
  const content = itemRole === "tool-result" ? unavailableContent("tool-output-in-activity-preview") : (() => {
133
151
  const projected = safeText(itemRole === "user" ? userProjection.text : messageText(entry.message));
134
- const failureReason = projected.full.length === 0 ? assistantFailureReason(entry.message) : null;
135
- if (failureReason) return unavailableContent(failureReason);
152
+ const unavailableReason = assistantUnavailableReason(entry.message, projected.full, projectedToolCalls);
153
+ if (unavailableReason) return unavailableContent(unavailableReason);
136
154
  return { state: projected.redacted ? "redacted" as const : "available" as const, text: projected.preview,
137
155
  textChars: Math.min(1_000_000_000, projected.full.length), digest: `sha256:${hash(projected.full)}`, truncated: projected.truncated,
138
156
  redacted: projected.redacted, imageCount: imageCount(entry.message), reasonCode: projected.redacted ? "sensitive-values-redacted" as const : null };
139
157
  })();
140
158
  return { messageRef: opaque("message", [identity.sessionRef, entry.id]), parentMessageRef: null, role: itemRole, recordedAt,
141
159
  agentOperationId: null, turnIndex: null, content, ...(userProjection.attachments.length ? { attachments: userProjection.attachments } : {}),
142
- toolCalls: toolCalls(entry.message, identity.sessionRef, itemRole) };
160
+ toolCalls: projectedToolCalls };
143
161
  }
144
162
  function unavailable(input: TranscriptProjectionInput, reasonCode: string, limit: number): TranscriptDocument {
145
163
  return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),