@wrenai/genbi 0.0.3 → 0.0.5

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 (230) hide show
  1. package/README.md +65 -3
  2. package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
  3. package/dist/assets/index-A_Wrq7Ey.css +1 -0
  4. package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
  5. package/dist/index.html +2 -2
  6. package/dist-server/harness/bundle/schema.d.ts +38 -0
  7. package/dist-server/harness/bundle/schema.js +18 -10
  8. package/dist-server/harness/bundle/version.js +1 -1
  9. package/dist-server/harness/cli-args.d.ts +10 -2
  10. package/dist-server/harness/cli-args.js +24 -8
  11. package/dist-server/harness/cli.js +75 -9
  12. package/dist-server/harness/compile/cache.js +4 -4
  13. package/dist-server/harness/compile/compose-profile.d.ts +51 -14
  14. package/dist-server/harness/compile/compose-profile.js +85 -19
  15. package/dist-server/harness/compile/context-loader.d.ts +56 -0
  16. package/dist-server/harness/compile/context-loader.js +237 -0
  17. package/dist-server/harness/compile/errors.d.ts +19 -0
  18. package/dist-server/harness/compile/errors.js +35 -0
  19. package/dist-server/harness/compile/index.d.ts +4 -3
  20. package/dist-server/harness/compile/index.js +4 -3
  21. package/dist-server/harness/compile/pipeline.d.ts +15 -7
  22. package/dist-server/harness/compile/pipeline.js +62 -14
  23. package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
  24. package/dist-server/harness/compile/resolve-binary.js +13 -3
  25. package/dist-server/harness/compile/types.d.ts +29 -0
  26. package/dist-server/harness/compile/warble-identity.d.ts +11 -0
  27. package/dist-server/harness/compile/warble-identity.js +13 -0
  28. package/dist-server/harness/components/ai-step.d.ts +4 -0
  29. package/dist-server/harness/components/ai-step.js +40 -0
  30. package/dist-server/harness/components/broker.d.ts +60 -0
  31. package/dist-server/harness/components/broker.js +117 -0
  32. package/dist-server/harness/components/capability-card.d.ts +98 -0
  33. package/dist-server/harness/components/capability-card.js +169 -0
  34. package/dist-server/harness/components/context.d.ts +2 -0
  35. package/dist-server/harness/components/context.js +30 -0
  36. package/dist-server/harness/components/display.d.ts +6 -0
  37. package/dist-server/harness/components/display.js +29 -0
  38. package/dist-server/harness/components/egress-judge.d.ts +12 -0
  39. package/dist-server/harness/components/egress-judge.js +27 -0
  40. package/dist-server/harness/components/egress.d.ts +142 -0
  41. package/dist-server/harness/components/egress.js +469 -0
  42. package/dist-server/harness/components/in-process.d.ts +7 -0
  43. package/dist-server/harness/components/in-process.js +245 -0
  44. package/dist-server/harness/components/normalize.d.ts +35 -0
  45. package/dist-server/harness/components/normalize.js +245 -0
  46. package/dist-server/harness/components/plan.d.ts +31 -0
  47. package/dist-server/harness/components/plan.js +184 -0
  48. package/dist-server/harness/components/report.d.ts +207 -0
  49. package/dist-server/harness/components/report.js +309 -0
  50. package/dist-server/harness/components/runner.d.ts +193 -0
  51. package/dist-server/harness/components/runner.js +492 -0
  52. package/dist-server/harness/components/semantic-guards.d.ts +20 -0
  53. package/dist-server/harness/components/semantic-guards.js +42 -0
  54. package/dist-server/harness/components/terminal-json.d.ts +18 -0
  55. package/dist-server/harness/components/terminal-json.js +82 -0
  56. package/dist-server/harness/components/usage.d.ts +14 -0
  57. package/dist-server/harness/components/usage.js +19 -0
  58. package/dist-server/harness/components/wren-access.d.ts +29 -0
  59. package/dist-server/harness/components/wren-access.js +233 -0
  60. package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
  61. package/dist-server/harness/components/zone-dry-run.js +107 -0
  62. package/dist-server/harness/components/zone-gate.d.ts +76 -0
  63. package/dist-server/harness/components/zone-gate.js +144 -0
  64. package/dist-server/harness/events/index.d.ts +1 -1
  65. package/dist-server/harness/events/types.d.ts +52 -0
  66. package/dist-server/harness/index.d.ts +9 -2
  67. package/dist-server/harness/index.js +5 -1
  68. package/dist-server/harness/loop/executor.d.ts +1 -0
  69. package/dist-server/harness/loop/executor.js +37 -15
  70. package/dist-server/harness/npm-package-resolve.d.ts +2 -0
  71. package/dist-server/harness/npm-package-resolve.js +16 -4
  72. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
  73. package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
  74. package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
  75. package/dist-server/harness/providers/adapters/openai.js +6 -0
  76. package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
  77. package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
  78. package/dist-server/harness/providers/binding.d.ts +45 -4
  79. package/dist-server/harness/providers/binding.js +44 -8
  80. package/dist-server/harness/providers/index.d.ts +9 -3
  81. package/dist-server/harness/providers/index.js +10 -3
  82. package/dist-server/harness/providers/zone.d.ts +96 -0
  83. package/dist-server/harness/providers/zone.js +112 -0
  84. package/dist-server/harness/render/answering-query.d.ts +29 -0
  85. package/dist-server/harness/render/answering-query.js +62 -0
  86. package/dist-server/harness/render/envelope.d.ts +6 -0
  87. package/dist-server/harness/render/envelope.js +10 -3
  88. package/dist-server/harness/route/adapter-spec.d.ts +8 -9
  89. package/dist-server/harness/route/adapter-spec.js +13 -14
  90. package/dist-server/harness/route/codex-ask-events.js +2 -1
  91. package/dist-server/harness/route/codex-ask.d.ts +4 -0
  92. package/dist-server/harness/route/codex-ask.js +69 -30
  93. package/dist-server/harness/route/codex-host-query.d.ts +52 -0
  94. package/dist-server/harness/route/codex-host-query.js +191 -0
  95. package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
  96. package/dist-server/harness/route/codex-local-manifest.js +96 -15
  97. package/dist-server/harness/route/codex-wren-proxy.d.ts +33 -0
  98. package/dist-server/harness/route/codex-wren-proxy.js +227 -0
  99. package/dist-server/harness/route/describe.js +45 -6
  100. package/dist-server/harness/route/in-process.d.ts +22 -1
  101. package/dist-server/harness/route/in-process.js +54 -4
  102. package/dist-server/harness/route/route.js +5 -2
  103. package/dist-server/harness/route/types.d.ts +30 -1
  104. package/dist-server/harness/session/run.js +30 -10
  105. package/dist-server/harness/setup/runner.d.ts +2 -0
  106. package/dist-server/harness/setup/runner.js +38 -17
  107. package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
  108. package/dist-server/harness/setup/yaml-scalar.js +62 -0
  109. package/dist-server/server/adopt.js +1 -17
  110. package/dist-server/server/app.d.ts +1 -0
  111. package/dist-server/server/app.js +163 -14
  112. package/dist-server/server/auth-choice.d.ts +13 -1
  113. package/dist-server/server/auth-choice.js +66 -1
  114. package/dist-server/server/bin.js +197 -26
  115. package/dist-server/server/codex-local-grounding.d.ts +18 -0
  116. package/dist-server/server/codex-local-grounding.js +92 -0
  117. package/dist-server/server/codex-native-session.d.ts +25 -0
  118. package/dist-server/server/codex-native-session.js +1 -0
  119. package/dist-server/server/codex-runtime-composition.d.ts +34 -0
  120. package/dist-server/server/codex-runtime-composition.js +250 -0
  121. package/dist-server/server/codex-structured-ask.d.ts +23 -0
  122. package/dist-server/server/codex-structured-ask.js +102 -0
  123. package/dist-server/server/conn-config.js +4 -16
  124. package/dist-server/server/conversation-socket.d.ts +16 -0
  125. package/dist-server/server/conversation-socket.js +65 -0
  126. package/dist-server/server/db.d.ts +46 -0
  127. package/dist-server/server/db.js +88 -9
  128. package/dist-server/server/env-detect.d.ts +6 -0
  129. package/dist-server/server/env-detect.js +7 -0
  130. package/dist-server/server/fold.d.ts +22 -4
  131. package/dist-server/server/fold.js +37 -5
  132. package/dist-server/server/harness.d.ts +2 -2
  133. package/dist-server/server/harness.js +33 -7
  134. package/dist-server/server/interactive-terminal.d.ts +10 -6
  135. package/dist-server/server/interactive-terminal.js +33 -8
  136. package/dist-server/server/managed-wren-runtime.d.ts +116 -0
  137. package/dist-server/server/managed-wren-runtime.js +670 -0
  138. package/dist-server/server/native-artifacts.d.ts +23 -7
  139. package/dist-server/server/native-artifacts.js +43 -12
  140. package/dist-server/server/native-component-context.d.ts +19 -0
  141. package/dist-server/server/native-component-context.js +76 -0
  142. package/dist-server/server/native-component-preparation.d.ts +20 -0
  143. package/dist-server/server/native-component-preparation.js +129 -0
  144. package/dist-server/server/native-component-runtime.d.ts +36 -0
  145. package/dist-server/server/native-component-runtime.js +92 -0
  146. package/dist-server/server/native-components.d.ts +84 -0
  147. package/dist-server/server/native-components.js +160 -0
  148. package/dist-server/server/native-dispatch-process.d.ts +8 -0
  149. package/dist-server/server/native-dispatch-process.js +65 -0
  150. package/dist-server/server/native-runtime-spec.d.ts +89 -0
  151. package/dist-server/server/native-runtime-spec.js +169 -0
  152. package/dist-server/server/native-sessions.d.ts +172 -19
  153. package/dist-server/server/native-sessions.js +870 -251
  154. package/dist-server/server/native-shutdown.d.ts +6 -0
  155. package/dist-server/server/native-shutdown.js +28 -0
  156. package/dist-server/server/native-wren-home.d.ts +18 -0
  157. package/dist-server/server/native-wren-home.js +262 -0
  158. package/dist-server/server/node-pty-host.d.ts +2 -1
  159. package/dist-server/server/node-pty-host.js +3 -2
  160. package/dist-server/server/profile-registry.d.ts +139 -0
  161. package/dist-server/server/profile-registry.js +620 -0
  162. package/dist-server/server/runtime-binding.d.ts +4 -1
  163. package/dist-server/server/runtime-binding.js +50 -2
  164. package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
  165. package/dist-server/server/runtime-host/claude-component-step.js +186 -0
  166. package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
  167. package/dist-server/server/runtime-host/codex-app-server.js +239 -0
  168. package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
  169. package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
  170. package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
  171. package/dist-server/server/runtime-host/codex-component-step.js +308 -0
  172. package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
  173. package/dist-server/server/runtime-host/codex-conversation.js +295 -0
  174. package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
  175. package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
  176. package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
  177. package/dist-server/server/runtime-host/codex-events.js +66 -0
  178. package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
  179. package/dist-server/server/runtime-host/codex-policy.js +151 -0
  180. package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
  181. package/dist-server/server/runtime-host/codex-process.js +96 -0
  182. package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
  183. package/dist-server/server/runtime-host/codex-rpc.js +182 -0
  184. package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
  185. package/dist-server/server/runtime-host/codex-session.js +492 -0
  186. package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
  187. package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
  188. package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
  189. package/dist-server/server/runtime-host/codex-terminal.js +120 -0
  190. package/dist-server/server/runtime-host/index.d.ts +5 -0
  191. package/dist-server/server/runtime-host/index.js +3 -0
  192. package/dist-server/server/runtime-host/local.d.ts +29 -0
  193. package/dist-server/server/runtime-host/local.js +90 -0
  194. package/dist-server/server/runtime-host/policy.d.ts +8 -0
  195. package/dist-server/server/runtime-host/policy.js +78 -0
  196. package/dist-server/server/runtime-host/types.d.ts +52 -0
  197. package/dist-server/server/runtime-host/types.js +62 -0
  198. package/dist-server/server/structured-query-evidence.d.ts +7 -0
  199. package/dist-server/server/structured-query-evidence.js +47 -0
  200. package/dist-server/server/structured-runtime.d.ts +52 -0
  201. package/dist-server/server/structured-runtime.js +130 -0
  202. package/dist-server/server/structured-scope.d.ts +4 -0
  203. package/dist-server/server/structured-scope.js +12 -0
  204. package/dist-server/server/turn.d.ts +6 -1
  205. package/dist-server/server/turn.js +82 -25
  206. package/dist-server/server/wire-types.d.ts +67 -3
  207. package/dist-server/server/wren-profiles.js +1 -1
  208. package/managed-wren/README.md +60 -0
  209. package/managed-wren/dependency-wheels.json +282 -0
  210. package/managed-wren/manifest.json +373 -0
  211. package/managed-wren/release-inputs.json +45 -0
  212. package/managed-wren/runtime-tree.cjs +115 -0
  213. package/managed-wren/source-inputs.json +40 -0
  214. package/package.json +15 -6
  215. package/profiles/genbi-default/context/binding.yml +17 -7
  216. package/profiles/genbi-default/context/context.json +146 -0
  217. package/profiles/genbi-default/ir.golden.json +39 -43
  218. package/profiles/genbi-default/profile.yml +31 -0
  219. package/profiles/genbi-enrich-context/context/binding.yml +14 -3
  220. package/profiles/genbi-enrich-context/context/context.json +146 -0
  221. package/profiles/genbi-enrich-context/ir.golden.json +4 -5
  222. package/profiles/genbi-monitor/context/binding.yml +13 -5
  223. package/profiles/genbi-monitor/context/context.json +146 -0
  224. package/profiles/genbi-monitor/ir.golden.json +13 -7
  225. package/profiles/genbi-report/context/binding.yml +19 -0
  226. package/profiles/genbi-report/context/context.json +223 -0
  227. package/profiles/genbi-report/ir.golden.json +328 -0
  228. package/profiles/genbi-report/profile.yml +38 -0
  229. package/profiles/genbi-setup/ir.golden.json +3 -1
  230. package/dist/assets/index-Xs4q7pNo.css +0 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The session runtime boundary is intentionally smaller than the existing
3
+ * native-session lifecycle. This module describes what the browser may know
4
+ * about a backend; process details remain in the host-only diagnostic surface.
5
+ */
6
+ export declare const RUNTIME_BACKEND_IDS: readonly ["local", "codex-app-server", "claude-sandbox-runtime"];
7
+ export type RuntimeBackendId = (typeof RUNTIME_BACKEND_IDS)[number];
8
+ export declare const RUNTIME_CAPABILITIES: readonly ["pty", "terminal_replay", "terminal_resize", "terminal_terminate", "app_server_rpc", "sandbox_policy", "filesystem_isolation", "network_isolation"];
9
+ export type RuntimeCapability = (typeof RUNTIME_CAPABILITIES)[number];
10
+ export type RuntimeReadinessState = "ready" | "unavailable" | "incompatible" | "unprovisioned";
11
+ export declare const RUNTIME_BACKEND_REASON_CODES: {
12
+ readonly local: readonly ["local_runtime_disabled", "local_runtime_host_unavailable", "local_runtime_production_forbidden", "runtime_probe_failed"];
13
+ readonly "codex-app-server": readonly ["codex_app_server_unprovisioned", "codex_cli_missing", "codex_cli_version_malformed", "codex_cli_version_unsupported", "codex_identity_uncertified", "codex_dependencies_drifted", "codex_app_server_unreachable", "codex_app_server_cleanup_failed", "codex_app_server_protocol_incompatible", "codex_sandbox_policy_unavailable", "codex_permission_profile_unavailable", "codex_wren_manifest_missing", "codex_wren_manifest_invalid", "codex_wren_platform_unsupported", "codex_wren_runtime_unprovisioned", "codex_wren_provision_failed", "codex_wren_interpreter_mismatch", "codex_wren_launcher_mismatch", "codex_wren_package_mismatch", "codex_wren_closure_mismatch", "codex_wren_profile_unavailable", "codex_wren_secret_unavailable", "codex_wren_session_home_unavailable", "codex_wren_child_env_invalid", "runtime_platform_unsupported", "runtime_policy_unavailable", "runtime_probe_failed"];
14
+ readonly "claude-sandbox-runtime": readonly ["claude_sandbox_runtime_unprovisioned", "claude_cli_missing", "claude_cli_version_malformed", "claude_cli_version_unsupported", "claude_identity_uncertified", "claude_sandbox_runtime_missing", "claude_sandbox_runtime_version_malformed", "claude_sandbox_runtime_version_unsupported", "claude_sandbox_capability_missing", "runtime_platform_unsupported", "runtime_policy_unavailable", "runtime_probe_failed"];
15
+ };
16
+ export type RuntimeBackendReasonCode<B extends RuntimeBackendId = RuntimeBackendId> = (typeof RUNTIME_BACKEND_REASON_CODES)[B][number];
17
+ export interface RuntimeBackendReady {
18
+ readonly state: "ready";
19
+ /** Certified or development-local version token; never command output. */
20
+ readonly version: string;
21
+ /** Public capability names only. */
22
+ readonly capabilities: readonly RuntimeCapability[];
23
+ }
24
+ export interface RuntimeBackendNotReady<B extends RuntimeBackendId = RuntimeBackendId> {
25
+ readonly state: "unavailable" | "incompatible" | "unprovisioned";
26
+ readonly code: RuntimeBackendReasonCode<B>;
27
+ /** Fixed text derived from `code`, never from a command, path, or environment. */
28
+ readonly message: string;
29
+ }
30
+ export type RuntimeBackendReadiness<B extends RuntimeBackendId = RuntimeBackendId> = RuntimeBackendReady | RuntimeBackendNotReady<B>;
31
+ /** Browser/API-safe projection. The selected result is always this record's own member. */
32
+ export interface RuntimeReadiness {
33
+ readonly selected: RuntimeBackendId;
34
+ readonly selectedReadiness: RuntimeBackendReadiness;
35
+ readonly backends: Readonly<Record<RuntimeBackendId, RuntimeBackendReadiness>>;
36
+ }
37
+ /**
38
+ * Host-only support evidence. Values are constrained to parsed tokens and
39
+ * declared capabilities; no raw command output, filesystem path, credential,
40
+ * or environment value belongs in this type.
41
+ */
42
+ export interface RuntimeBackendDiagnostic {
43
+ readonly phase: "policy" | "platform" | "version" | "identity" | "capability" | "provisioning" | "probe";
44
+ readonly observedVersion?: string;
45
+ readonly requiredCapability?: RuntimeCapability;
46
+ readonly attestedIdentity?: `sha256:${string}`;
47
+ }
48
+ export type RuntimeDiagnostics = Readonly<Record<RuntimeBackendId, RuntimeBackendDiagnostic>>;
49
+ export interface RuntimeBackendProbeResult<B extends RuntimeBackendId = RuntimeBackendId> {
50
+ readonly readiness: RuntimeBackendReadiness<B>;
51
+ readonly diagnostic: RuntimeBackendDiagnostic;
52
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The session runtime boundary is intentionally smaller than the existing
3
+ * native-session lifecycle. This module describes what the browser may know
4
+ * about a backend; process details remain in the host-only diagnostic surface.
5
+ */
6
+ export const RUNTIME_BACKEND_IDS = ["local", "codex-app-server", "claude-sandbox-runtime"];
7
+ export const RUNTIME_CAPABILITIES = [
8
+ "pty",
9
+ "terminal_replay",
10
+ "terminal_resize",
11
+ "terminal_terminate",
12
+ "app_server_rpc",
13
+ "sandbox_policy",
14
+ "filesystem_isolation",
15
+ "network_isolation",
16
+ ];
17
+ export const RUNTIME_BACKEND_REASON_CODES = {
18
+ local: ["local_runtime_disabled", "local_runtime_host_unavailable", "local_runtime_production_forbidden", "runtime_probe_failed"],
19
+ "codex-app-server": [
20
+ "codex_app_server_unprovisioned",
21
+ "codex_cli_missing",
22
+ "codex_cli_version_malformed",
23
+ "codex_cli_version_unsupported",
24
+ "codex_identity_uncertified",
25
+ "codex_dependencies_drifted",
26
+ "codex_app_server_unreachable",
27
+ "codex_app_server_cleanup_failed",
28
+ "codex_app_server_protocol_incompatible",
29
+ "codex_sandbox_policy_unavailable",
30
+ "codex_permission_profile_unavailable",
31
+ "codex_wren_manifest_missing",
32
+ "codex_wren_manifest_invalid",
33
+ "codex_wren_platform_unsupported",
34
+ "codex_wren_runtime_unprovisioned",
35
+ "codex_wren_provision_failed",
36
+ "codex_wren_interpreter_mismatch",
37
+ "codex_wren_launcher_mismatch",
38
+ "codex_wren_package_mismatch",
39
+ "codex_wren_closure_mismatch",
40
+ "codex_wren_profile_unavailable",
41
+ "codex_wren_secret_unavailable",
42
+ "codex_wren_session_home_unavailable",
43
+ "codex_wren_child_env_invalid",
44
+ "runtime_platform_unsupported",
45
+ "runtime_policy_unavailable",
46
+ "runtime_probe_failed",
47
+ ],
48
+ "claude-sandbox-runtime": [
49
+ "claude_sandbox_runtime_unprovisioned",
50
+ "claude_cli_missing",
51
+ "claude_cli_version_malformed",
52
+ "claude_cli_version_unsupported",
53
+ "claude_identity_uncertified",
54
+ "claude_sandbox_runtime_missing",
55
+ "claude_sandbox_runtime_version_malformed",
56
+ "claude_sandbox_runtime_version_unsupported",
57
+ "claude_sandbox_capability_missing",
58
+ "runtime_platform_unsupported",
59
+ "runtime_policy_unavailable",
60
+ "runtime_probe_failed",
61
+ ],
62
+ };
@@ -0,0 +1,7 @@
1
+ import type { RenderEnvelope } from "../harness/render/envelope.js";
2
+ /** Only call with returns from scoped host tools, never vendor messages or tool arguments.
3
+ * Keep every root in call order; mixed failure/success must not look wholly verified.
4
+ */
5
+ export declare function structuredQueryEnvelope(roots: readonly unknown[], explanation: string): RenderEnvelope | undefined;
6
+ /** Supply the clock explicitly. Historical datasets must never silently become 'recent'. */
7
+ export declare function analyticalAskPrompt(question: string, now: Date): string;
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ const cell = z.union([z.string(), z.number().finite(), z.boolean(), z.null()]);
3
+ const table = z.object({ columns: z.array(z.string()).min(1), rows: z.array(z.union([z.array(cell), z.record(z.string(), cell)])) });
4
+ const definition = z.object({ sql: z.string().trim().min(1), source_tables: z.array(z.string()).optional(), filters: z.array(z.string()).optional() });
5
+ const renderResult = z.object({ status: z.literal("ok"),
6
+ output: z.object({ kind: z.literal("render"), blocks: z.array(z.object({ type: z.string() }).passthrough()) }),
7
+ provenance: z.object({ verified: z.literal(true) }) });
8
+ const result = z.object({ status: z.literal("ok"), output: z.object({ kind: z.literal("value"), value: table }),
9
+ provenance: z.object({ verified: z.literal(true), definition }) });
10
+ /** Only call with returns from scoped host tools, never vendor messages or tool arguments.
11
+ * Keep every root in call order; mixed failure/success must not look wholly verified.
12
+ */
13
+ export function structuredQueryEnvelope(roots, explanation) {
14
+ if (!roots.length)
15
+ return undefined;
16
+ const blocks = [];
17
+ for (const raw of roots) {
18
+ const parsed = result.safeParse(raw);
19
+ if (!parsed.success) {
20
+ const rendered = renderResult.safeParse(raw);
21
+ if (!rendered.success || !rendered.data.output.blocks.length)
22
+ return undefined;
23
+ blocks.push(...rendered.data.output.blocks);
24
+ continue;
25
+ }
26
+ const { columns, rows } = parsed.data.output.value;
27
+ if (new Set(columns).size !== columns.length || rows.some((row) => Array.isArray(row)
28
+ ? row.length !== columns.length : columns.some((name) => !Object.hasOwn(row, name))))
29
+ return undefined;
30
+ blocks.push({ type: "table", columns, rows }, { type: "definition", ...parsed.data.provenance.definition });
31
+ }
32
+ return { blocks, verified: true, verificationScope: "query-results", explanation };
33
+ }
34
+ /** Supply the clock explicitly. Historical datasets must never silently become 'recent'. */
35
+ export function analyticalAskPrompt(question, now) {
36
+ return `Current reference time (UTC): ${now.toISOString()}.
37
+ Use the selected analysis tool for data questions and pass the user's requested period and metric definitions faithfully.
38
+ If no period was specified, use the full available data range and disclose this assumption; do not invent a recent N-week limit.
39
+ For relative periods, distinguish the current calendar from the latest data date. Never silently re-anchor a calendar-relative request.
40
+ The application displays successful query rows and SQL directly. Do not repeat the table or serialize an answer envelope in your final text.
41
+ Instead explain in the user's language: exact date range and anchor, date field, week start, timezone (or unknown), partial periods,
42
+ and the definition of each metric. Separate context-defined metrics from assumptions. Do not invent signup dates or duplicate synonymous metrics.
43
+ If the result cannot establish a date bound or definition, state that it is unknown or ask for clarification. Do not invent evidence.
44
+
45
+ User question:
46
+ ${question}`;
47
+ }
@@ -0,0 +1,52 @@
1
+ import { type AuthChoice, type RouteOptions, type RouteResult, type SetupStepRunner, type SetupStepRunOptions, type SetupStepRunResult } from "../harness/index.js";
2
+ import type { RuntimeBackendReadiness } from "./runtime-host/types.js";
3
+ export type StructuredVendor = "codex" | "claude";
4
+ export type StructuredBackend = "local" | "codex-app-server" | "claude-sandbox-runtime";
5
+ export interface StructuredScope {
6
+ readonly sessionId: string;
7
+ readonly turnId: string;
8
+ readonly signal?: AbortSignal;
9
+ /** Captured host state, rechecked before/after preparation, tools, events and results. */
10
+ assertCurrent(): void;
11
+ }
12
+ export declare class StructuredRuntimeError extends Error {
13
+ readonly code: "unavailable" | "unsupported" | "stale" | "cancelled" | "failed" | "cleanup" | "timeout";
14
+ constructor(code: "unavailable" | "unsupported" | "stale" | "cancelled" | "failed" | "cleanup" | "timeout");
15
+ }
16
+ export interface StructuredAdapter {
17
+ readonly vendor: StructuredVendor;
18
+ readonly backend: Exclude<StructuredBackend, "local">;
19
+ probe(): Promise<RuntimeBackendReadiness>;
20
+ ask?(options: RouteOptions, scope: StructuredScope): Promise<RouteResult>;
21
+ setup?(options: SetupStepRunOptions, scope: StructuredScope): Promise<SetupStepRunResult>;
22
+ }
23
+ interface Options {
24
+ readonly selected: {
25
+ readonly codex: "local" | "codex-app-server";
26
+ readonly claude: "local" | "claude-sandbox-runtime";
27
+ };
28
+ /** Explicit compatibility opt-in, never proof of vendor sandboxing. */
29
+ readonly allowLocal: boolean;
30
+ readonly deployment?: RouteOptions["deployment"];
31
+ readonly localRoute: (options: RouteOptions) => Promise<RouteResult>;
32
+ readonly localSetupFor: (auth: AuthChoice) => SetupStepRunner | undefined;
33
+ readonly adapters?: Readonly<Partial<Record<StructuredVendor, StructuredAdapter>>>;
34
+ }
35
+ /** Server composition owns backend choice. No browser paths/configuration select a backend. */
36
+ export declare class StructuredRuntime {
37
+ private readonly options;
38
+ private readonly lifetime;
39
+ private readonly controllers;
40
+ private readonly active;
41
+ private cleanupFailed;
42
+ constructor(options: Options);
43
+ private adapter;
44
+ readiness(): Promise<any>;
45
+ route(options: RouteOptions, scope: StructuredScope): Promise<RouteResult>;
46
+ setup(options: SetupStepRunOptions, scope: StructuredScope): Promise<SetupStepRunResult>;
47
+ private run;
48
+ assertActive(): void;
49
+ revoke(): void;
50
+ shutdown(): Promise<void>;
51
+ }
52
+ export {};
@@ -0,0 +1,130 @@
1
+ import { enforceCompliance } from "../harness/index.js";
2
+ export class StructuredRuntimeError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`Structured runtime ${code}.`);
6
+ this.code = code;
7
+ }
8
+ }
9
+ /** Server composition owns backend choice. No browser paths/configuration select a backend. */
10
+ export class StructuredRuntime {
11
+ options;
12
+ lifetime = new AbortController();
13
+ controllers = new Set();
14
+ active = new Set();
15
+ cleanupFailed = false;
16
+ constructor(options) {
17
+ this.options = { ...options, selected: Object.freeze({ ...options.selected }), adapters: Object.freeze({ ...options.adapters }) };
18
+ }
19
+ adapter(vendor) {
20
+ const candidate = this.options.adapters?.[vendor];
21
+ return candidate?.vendor === vendor && candidate.backend === this.options.selected[vendor] ? candidate : undefined;
22
+ }
23
+ async readiness() {
24
+ const entries = await Promise.all(["codex", "claude"].map(async (vendor) => {
25
+ const backend = this.options.selected[vendor];
26
+ const localReady = this.options.allowLocal && !this.lifetime.signal.aborted;
27
+ if (backend === "local")
28
+ return [vendor, { backend, state: localReady ? "ready" : "unavailable", ask: localReady, setup: localReady && !!this.options.localSetupFor({ mode: "subscription", provider: vendor }), reason: this.options.allowLocal ? "Explicit local execution; vendor isolation is not claimed." : "Local execution is disabled." }];
29
+ const adapter = this.adapter(vendor);
30
+ let probe;
31
+ try {
32
+ probe = await adapter?.probe();
33
+ }
34
+ catch { /* fixed public failure below */ }
35
+ const ready = !this.lifetime.signal.aborted && probe?.state === "ready";
36
+ return [vendor, { backend, state: ready ? "ready" : "unavailable", ask: ready && !!adapter?.ask, setup: ready && !!adapter?.setup,
37
+ reason: !adapter ? "Selected vendor backend is not configured." : !ready ? "Selected vendor backend is not ready." : "Only explicitly supported operations are available.",
38
+ ...(probe && probe.state !== "ready" ? { code: probe.code } : {}) }];
39
+ }));
40
+ return Object.fromEntries(entries);
41
+ }
42
+ async route(options, scope) {
43
+ return this.run(options.authChoice, scope, async (adapter, guarded) => {
44
+ const request = { ...options, signal: guarded.signal, onEvent: (event) => { guarded.assertCurrent(); options.onEvent?.(event); } };
45
+ if (!adapter)
46
+ return this.options.localRoute(request);
47
+ if (!adapter.ask)
48
+ throw new StructuredRuntimeError("unsupported");
49
+ return adapter.ask(request, guarded);
50
+ }, options.deployment);
51
+ }
52
+ async setup(options, scope) {
53
+ return this.run(options.authChoice, scope, async (adapter, guarded) => {
54
+ const request = { ...options, signal: guarded.signal, onEvent: (event) => { guarded.assertCurrent(); options.onEvent?.(event); } };
55
+ if (!adapter) {
56
+ const runner = this.options.localSetupFor(options.authChoice);
57
+ if (!runner)
58
+ throw new StructuredRuntimeError("unavailable");
59
+ return runner.run(request);
60
+ }
61
+ if (!adapter.setup || options.resumeSessionId !== undefined)
62
+ throw new StructuredRuntimeError("unsupported");
63
+ // Isolated runners must not mint legacy provider-resume anchors.
64
+ const result = await adapter.setup(request, guarded);
65
+ return { finalText: result.finalText };
66
+ });
67
+ }
68
+ run(auth, scope, operation, deployment = "personal") {
69
+ const controller = new AbortController();
70
+ this.controllers.add(controller);
71
+ const signal = AbortSignal.any([controller.signal, this.lifetime.signal, ...(scope.signal ? [scope.signal] : [])]);
72
+ const assertCurrent = () => {
73
+ if (signal.aborted)
74
+ throw new StructuredRuntimeError("cancelled");
75
+ try {
76
+ scope.assertCurrent();
77
+ }
78
+ catch {
79
+ throw new StructuredRuntimeError("stale");
80
+ }
81
+ };
82
+ const pending = (async () => {
83
+ assertCurrent();
84
+ enforceCompliance(auth, { deployment: this.options.deployment ?? deployment });
85
+ let adapter;
86
+ if (auth.mode === "subscription" && this.options.selected[auth.provider] !== "local") {
87
+ adapter = this.adapter(auth.provider);
88
+ if (!adapter)
89
+ throw new StructuredRuntimeError("unavailable");
90
+ let ready;
91
+ try {
92
+ ready = await adapter.probe();
93
+ }
94
+ catch {
95
+ throw new StructuredRuntimeError("unavailable");
96
+ }
97
+ assertCurrent();
98
+ if (ready.state !== "ready")
99
+ throw new StructuredRuntimeError("unavailable");
100
+ }
101
+ else if (auth.mode === "subscription" && !this.options.allowLocal)
102
+ throw new StructuredRuntimeError("unavailable");
103
+ let result;
104
+ try {
105
+ result = await operation(adapter, { sessionId: scope.sessionId, turnId: scope.turnId, signal, assertCurrent });
106
+ }
107
+ catch (error) {
108
+ if (adapter && !(error instanceof StructuredRuntimeError))
109
+ throw new StructuredRuntimeError(signal.aborted ? "cancelled" : "failed");
110
+ throw error;
111
+ }
112
+ assertCurrent();
113
+ return result;
114
+ })();
115
+ this.active.add(pending);
116
+ void pending.catch((error) => { if (error instanceof StructuredRuntimeError && error.code === "cleanup")
117
+ this.cleanupFailed = true; }).finally(() => { this.active.delete(pending); this.controllers.delete(controller); });
118
+ return pending;
119
+ }
120
+ assertActive() { if (this.lifetime.signal.aborted)
121
+ throw new StructuredRuntimeError("cancelled"); }
122
+ revoke() { for (const controller of this.controllers)
123
+ controller.abort(); }
124
+ async shutdown() {
125
+ this.lifetime.abort();
126
+ await Promise.allSettled([...this.active]);
127
+ if (this.cleanupFailed)
128
+ throw new StructuredRuntimeError("cleanup");
129
+ }
130
+ }
@@ -0,0 +1,4 @@
1
+ import type { Store, TurnRow } from "./db.js";
2
+ import { type StructuredScope } from "./structured-runtime.js";
3
+ /** Captured host-only values are compared, never persisted or sent as model/browser authority. */
4
+ export declare function captureStructuredScope(store: Store, turn: TurnRow, getProject: () => string | undefined, signal?: AbortSignal): StructuredScope;
@@ -0,0 +1,12 @@
1
+ import { StructuredRuntimeError } from "./structured-runtime.js";
2
+ /** Captured host-only values are compared, never persisted or sent as model/browser authority. */
3
+ export function captureStructuredScope(store, turn, getProject, signal) {
4
+ const snapshot = () => JSON.stringify({ runtime: store.getRuntimeSettings(), generation: store.getNativeRuntimeBinding().generation,
5
+ binding: store.getEnrichmentBinding(), project: getProject(), ...(turn.setupStepKey !== null ? { form: store.getSetupConnectForm() } : {}) });
6
+ const captured = snapshot();
7
+ return { sessionId: turn.sessionId, turnId: turn.id, ...(signal ? { signal } : {}), assertCurrent() {
8
+ const current = store.getTurn(turn.id);
9
+ if (signal?.aborted || snapshot() !== captured || !current || current.sessionId !== turn.sessionId || current.resultKind !== null)
10
+ throw new StructuredRuntimeError("stale");
11
+ } };
12
+ }
@@ -1,3 +1,5 @@
1
+ import type { ProfileRegistry } from "./profile-registry.js";
2
+ import type { StructuredRuntime } from "./structured-runtime.js";
1
3
  import type { AuthChoice, Bundle, LoginProbe, RouteOptions, RouteResult, SetupStepRunner } from "../harness/index.js";
2
4
  import type { SubscriptionModelCatalog, SubscriptionProvider } from "./wire-types.js";
3
5
  import { type Store } from "./db.js";
@@ -23,6 +25,7 @@ export declare class ArtifactNotFoundError extends Error {
23
25
  }
24
26
  export interface TurnDeps {
25
27
  readonly store: Store;
28
+ readonly structuredRuntime?: StructuredRuntime;
26
29
  readonly route: (options: RouteOptions) => Promise<RouteResult>;
27
30
  /** A native vendor TUI host, deliberately independent from Ask/SSE. */
28
31
  readonly startInteractiveTerminal?: (input: {
@@ -48,6 +51,8 @@ export interface TurnDeps {
48
51
  readonly nativeSessions?: NativeSessionService;
49
52
  /** Session-scoped MCP persistence service; never used by Ask publish routes. */
50
53
  readonly nativeArtifacts?: NativeArtifactService;
54
+ /** Server-held registry of conversation profiles; absent means `/api/profiles` answers 503. */
55
+ readonly profileRegistry?: ProfileRegistry;
51
56
  readonly baseRouteOptions: Omit<RouteOptions, "question" | "onEvent">;
52
57
  /**
53
58
  * Ask/compile-bind only: compiles+loads the analysis profile's bundle.
@@ -64,7 +69,7 @@ export interface TurnDeps {
64
69
  * dependency resolves the corresponding server-owned profile source. It is
65
70
  * intentionally distinct from native-session IR/launch wiring.
66
71
  */
67
- readonly describeHarnessBundle?: (purpose: NativePurpose, options: Omit<RouteOptions, "question" | "onEvent">) => Promise<Bundle>;
72
+ readonly describeHarnessBundle?: (purpose: NativePurpose, options: Omit<RouteOptions, "question" | "onEvent">, profileSource?: string) => Promise<Bundle>;
68
73
  /**
69
74
  * Compiles the canonical post-bind profile without rebinding its context and
70
75
  * returns its declared tier names. Unlike `describeBundle`, this seam is
@@ -1,3 +1,4 @@
1
+ import { captureStructuredScope } from "./structured-scope.js";
1
2
  /**
2
3
  * Orchestrates one turn end-to-end: clarify pre-flight, context
3
4
  * composition, invoking `route()`, folding its result/events into the UI's
@@ -16,7 +17,7 @@ import { assertRuntimeSettingsDispatchable, materializeRuntimeRouteOptions } fro
16
17
  import { classifyClarify } from "./clarify.js";
17
18
  import { classifyIntent } from "./route-intent.js";
18
19
  import { composeClarifyFollowUp, composeInput } from "./compose.js";
19
- import { clarifyDecisionStep, extractTrace, foldTrace, gateDecisionStep, gateFailureStep, hostContractRecoveryStep, LiveWorkLog, redactPublicSetupText, sanitizeLiveSetupWorklog, sanitizePublicSetupWorklog, routeDecisionStep, summarizeResult, toAnswerOrRefusalEvent, toArtifactEvent, } from "./fold.js";
20
+ import { clarifyDecisionStep, extractTrace, foldTrace, gateDecisionStep, gateFailureStep, hostContractRecoveryStep, LiveWorkLog, redactPublicSetupText, sanitizeLiveSetupWorklog, sanitizePublicSetupWorklog, routeDecisionStep, summarizeResult, toAnswerOrRefusalEvent, toArtifactEvent, withHostVerification, } from "./fold.js";
20
21
  export class SessionNotFoundError extends Error {
21
22
  sessionId;
22
23
  constructor(sessionId) {
@@ -255,7 +256,7 @@ async function replayResolvedTurn(store, turn, emit) {
255
256
  }
256
257
  async function executeTurn(deps, session, turn, emit, signal) {
257
258
  if (turn.setupStepKey !== null) {
258
- await executeSetupTurn(deps, session, turn, emit);
259
+ await executeSetupTurn(deps, session, turn, emit, signal);
259
260
  return;
260
261
  }
261
262
  const liveLog = new LiveWorkLog();
@@ -285,20 +286,9 @@ async function executeTurn(deps, session, turn, emit, signal) {
285
286
  chain = chain.then(() => emit({ event: "worklog", data: merged }));
286
287
  }
287
288
  };
288
- let result;
289
- try {
290
- const routeOptions = {
291
- ...effectiveRouteOptions(deps),
292
- question: turn.composedInput ?? turn.question,
293
- onEvent,
294
- ...(signal !== undefined ? { signal } : {}),
295
- // the turn's own persisted agent id (classified once at postTurn time), so a turn
296
- // always resolves to the same agent whether executed live or (via a future replay path) re-run.
297
- ...(turn.agentId !== null ? { agentId: turn.agentId } : {}),
298
- };
299
- result = await deps.route(routeOptions);
300
- }
301
- catch (err) {
289
+ const runtimeScope = deps.structuredRuntime ? captureStructuredScope(deps.store, turn, () => resolveUserProject(deps), signal) : undefined;
290
+ const assertCurrent = () => { deps.structuredRuntime?.assertActive(); runtimeScope?.assertCurrent(); };
291
+ const failTurn = async (err) => {
302
292
  await chain;
303
293
  const message = err instanceof Error ? err.message : String(err);
304
294
  // Persist the PARTIAL work log on failure — the Route/Clarify
@@ -312,7 +302,25 @@ async function executeTurn(deps, session, turn, emit, signal) {
312
302
  deps.store.resolveTurn(turn.id, { backend: null, resultKind: "error", answerSummary: null, traceJson: JSON.stringify(partialWorklog), errorMessage: message });
313
303
  deps.store.updateSessionStatus(session.id, "active", null);
314
304
  await emit({ event: "error", data: { message } });
315
- return; // spec: on error emit ONLY the error frame, never a trailing done
305
+ };
306
+ let result;
307
+ try {
308
+ const routeOptions = {
309
+ ...effectiveRouteOptions(deps),
310
+ question: turn.composedInput ?? turn.question,
311
+ onEvent,
312
+ ...(signal !== undefined ? { signal } : {}),
313
+ // the turn's own persisted agent id (classified once at postTurn time), so a turn
314
+ // always resolves to the same agent whether executed live or (via a future replay path) re-run.
315
+ ...(turn.agentId !== null ? { agentId: turn.agentId } : {}),
316
+ };
317
+ result = deps.structuredRuntime
318
+ ? await deps.structuredRuntime.route(routeOptions, runtimeScope)
319
+ : await deps.route(routeOptions);
320
+ }
321
+ catch (err) {
322
+ await failTurn(err);
323
+ return;
316
324
  }
317
325
  await chain;
318
326
  // Prefer the live worklog snapshot (LLM step rows interleaved
@@ -339,7 +347,16 @@ async function executeTurn(deps, session, turn, emit, signal) {
339
347
  // artifact-producing component (`generate_dashboard`/`explain_change` —
340
348
  // NOT a plain `answer_query` table), persist it as a real artifact too, so
341
349
  // it's publishable regardless of which backend ran the turn.
342
- const artifactEvent = await maybeCreateDispatchedArtifact(deps, session, turn, result, terminalEvent);
350
+ let artifactEvent;
351
+ try {
352
+ assertCurrent();
353
+ artifactEvent = await maybeCreateDispatchedArtifact(deps, session, turn, result, terminalEvent, assertCurrent);
354
+ assertCurrent();
355
+ }
356
+ catch (err) {
357
+ await failTurn(err);
358
+ return;
359
+ }
343
360
  // Persist the turn as fully resolved BEFORE emitting any terminal frames.
344
361
  // If a client disconnect makes an emit below throw, the turn is already
345
362
  // resolved, so the next stream deterministically REPLAYS these stored frames
@@ -384,11 +401,13 @@ async function executeTurn(deps, session, turn, emit, signal) {
384
401
  * verbatim as JSON to a session-scoped file under the same artifacts root
385
402
  * the in-process `write_artifact` tool uses (`resolveArtifactsDir`) — self-
386
403
  * contained from the envelope, no dependency on the dispatcher's `htmlPath`
387
- * (never present in the NDJSON stream today). `verified` comes straight from
388
- * the envelope's own `verified` field, never hardcoded.
404
+ * (never present in the NDJSON stream today). `verified` comes from the
405
+ * envelope after {@link withHostVerification}, so an artifact is stored as
406
+ * verified only when host evidence grounds it (a codex:local answer citing
407
+ * this turn's host-run queries), never on the model's own claim.
389
408
  */
390
- async function maybeCreateDispatchedArtifact(deps, session, turn, result, terminalEvent) {
391
- if (result.backend !== "agent-sdk" && result.backend !== "codex-local")
409
+ async function maybeCreateDispatchedArtifact(deps, session, turn, result, terminalEvent, assertCurrent) {
410
+ if (result.backend !== "agent-sdk" && result.backend !== "codex-local" && result.backend !== "codex-app-server")
392
411
  return undefined;
393
412
  if (terminalEvent.kind !== "answer" || terminalEvent.answer.form !== "rich")
394
413
  return undefined;
@@ -396,7 +415,8 @@ async function maybeCreateDispatchedArtifact(deps, session, turn, result, termin
396
415
  return undefined;
397
416
  if (!(await isArtifactProducerAgent(deps, turn.agentId)))
398
417
  return undefined;
399
- const envelope = terminalEvent.answer.envelope;
418
+ assertCurrent();
419
+ const envelope = withHostVerification(result, terminalEvent.answer.envelope);
400
420
  const artifactKind = artifactKindForAgent(turn.agentId);
401
421
  const verified = envelope.verified === true;
402
422
  const sessionArtifactsDir = path.join(resolveArtifactsDir(effectiveRouteOptions(deps).outDir), session.id);
@@ -516,7 +536,7 @@ function turnRecoveryAnchor(turn, sessionId, authChoice) {
516
536
  * `status` field, which the frontend inspects (see the wire contract in the
517
537
  * final report). Only a parsed `error` persists as `resultKind: "error"`.
518
538
  */
519
- async function executeSetupTurn(deps, session, turn, emit) {
539
+ async function executeSetupTurn(deps, session, turn, emit, signal) {
520
540
  // Provider session anchors are server-only. Keep every anchor that becomes
521
541
  // known during this turn so a host-contract continuation that rotates its
522
542
  // anchor still redacts both the original and replacement from the combined
@@ -645,7 +665,11 @@ async function executeSetupTurn(deps, session, turn, emit) {
645
665
  const data = initialAuthChoice.mode === "subscription" ? sanitizeLiveSetupWorklog(snapshot) : publicWorklog(snapshot);
646
666
  chain = chain.then(() => emit({ event: "worklog", data }));
647
667
  };
648
- const runAttempt = (prompt, authChoice, resumeSessionId) => setupRunner.run({
668
+ const runtimeScope = captureStructuredScope(deps.store, turn, () => resolveUserProject(deps), signal);
669
+ const runSelected = (options) => deps.structuredRuntime
670
+ ? deps.structuredRuntime.setup(options, runtimeScope) : setupRunner.run(options);
671
+ const runAttempt = (prompt, authChoice, resumeSessionId) => runSelected({
672
+ ...(signal ? { signal } : {}),
649
673
  prompt,
650
674
  workspaceRoot,
651
675
  // The form was validated before persistence. Thread it to dispatched so
@@ -744,6 +768,16 @@ async function executeSetupTurn(deps, session, turn, emit) {
744
768
  return;
745
769
  }
746
770
  await chain.catch(() => { });
771
+ try {
772
+ if (deps.structuredRuntime) {
773
+ deps.structuredRuntime.assertActive();
774
+ runtimeScope.assertCurrent();
775
+ }
776
+ }
777
+ catch (error) {
778
+ await failWithError(error instanceof Error ? error.message : "Structured runtime stale.", liveLog.snapshot());
779
+ return;
780
+ }
747
781
  const worklog = liveLog.snapshot();
748
782
  let terminal = parseTerminalWithRetainedLifecycle(finalText, worklog);
749
783
  terminal = { ...terminal, message: redactPublicSetupText(terminal.message) };
@@ -816,6 +850,16 @@ async function executeSetupTurn(deps, session, turn, emit) {
816
850
  return;
817
851
  }
818
852
  }
853
+ try {
854
+ if (deps.structuredRuntime) {
855
+ deps.structuredRuntime.assertActive();
856
+ runtimeScope.assertCurrent();
857
+ }
858
+ }
859
+ catch (error) {
860
+ await failWithError(error instanceof Error ? error.message : "Structured runtime stale.", liveLog.snapshot());
861
+ return;
862
+ }
819
863
  // A completed runner result can introduce a new server-owned anchor after
820
864
  // the live frames have begun. Re-sanitize every normal terminal boundary
821
865
  // with that now-known identity before it reaches SSE or SQLite.
@@ -882,6 +926,8 @@ async function executeSetupTurn(deps, session, turn, emit) {
882
926
  if (temporarilyBoundForHealthcheck) {
883
927
  deps.bindProject(path.join(workspaceRoot, form.projectName));
884
928
  }
929
+ // Refresh only after the host-owned bind, then fence the asynchronous healthcheck.
930
+ const healthcheckScope = captureStructuredScope(deps.store, turn, () => resolveUserProject(deps), signal);
885
931
  let compileHealthcheckFailure;
886
932
  if (!deps.describeBundle) {
887
933
  compileHealthcheckFailure = "the profile compile healthcheck is not configured";
@@ -894,6 +940,17 @@ async function executeSetupTurn(deps, session, turn, emit) {
894
940
  compileHealthcheckFailure = summarizeCompileHealthcheckFailure(err);
895
941
  }
896
942
  }
943
+ try {
944
+ if (deps.structuredRuntime) {
945
+ deps.structuredRuntime.assertActive();
946
+ healthcheckScope.assertCurrent();
947
+ }
948
+ }
949
+ catch (error) {
950
+ // A newer binding belongs to its new owner; never roll it back here.
951
+ await failWithError(error instanceof Error ? error.message : "Structured runtime stale.", finalWorklog);
952
+ return;
953
+ }
897
954
  if (compileHealthcheckFailure !== undefined) {
898
955
  if (temporarilyBoundForHealthcheck)
899
956
  deps.unbindProject?.();