@visual-z/xeno 0.8.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,149 +0,0 @@
1
- /** R8: live capabilities stay with their Session; only observations cross a log boundary. */
2
- import { randomUUID } from "node:crypto";
3
- import { hostname } from "node:os";
4
- import { join, resolve } from "node:path";
5
- import type { LogEvent, ArtifactRef } from "../core/events.ts";
6
- import { deriveAcceptance, type AcceptanceState } from "../core/acceptance.ts";
7
- import { canonicalValue } from "../core/continuation.ts";
8
- import { isCommandArtifactReceipt, sameArtifactIdentity, type CommandArtifactReceipt } from "../core/command-artifacts.ts";
9
- import { targetIsReady, type RuntimeTarget, type RuntimeTargetReceipt, type RuntimeInstance } from "../core/runtime-target.ts";
10
- import { failureDiagnostic, ToolFault, NOT_STARTED, UNKNOWN_EFFECT } from "../core/tool-failure.ts";
11
- import { commandTimeout } from "../core/command-contract.ts";
12
- import { executionOutcome } from "../core/outcome.ts";
13
- import { archiveCommandArtifacts, artifactExclusions, observeArtifactFiles, observeArtifactInputs } from "../store/command-artifacts.ts";
14
- import { readObservation } from "../store/observations.ts";
15
- import { targetDirectory, targetProgram, observeRuntimeIdentity } from "../store/runtime-target.ts";
16
- import { findSession, startSession, pollSession, yieldWait } from "./exec-sessions.ts";
17
- import { sessionResult, type ToolContext, type ToolResult } from "./tools.ts";
18
-
19
- type History = readonly { node: number; events: readonly LogEvent[] }[];
20
- type LiveTarget = { request: string; contract: string; build: string; instance: RuntimeInstance };
21
- type Request = { action: "start" | "inspect"; target: string; session_id?: number; yield_time_ms?: number; timeout?: number };
22
- // Assumed bound for the small build manifest; binaries are observed separately, never read from log JSON.
23
- const MANIFEST_BYTES = 256 * 1024;
24
-
25
- export class RuntimeTargets {
26
- private readonly live = new Map<string, LiveTarget>();
27
-
28
- async perform(ctx: ToolContext, history: History, input: Request): Promise<{ result: ToolResult; observation: RuntimeTargetReceipt }> {
29
- const events = history.flatMap(part => [...part.events]);
30
- const state = deriveAcceptance(undefined, events);
31
- const contract = events.find(event => event.id === state?.contract_event);
32
- const target = contract?.t === "acceptance_contract" ? contract.contract.targets?.find(item => item.id === input.target) : undefined;
33
- if (!state || !target) throw new ToolFault("invalid_input", "target must name a runtime in the current trusted acceptance contract", NOT_STARTED);
34
- const archive = ctx.outputStore ?? join(ctx.root, ".xeno", "observations");
35
- const receipt: RuntimeTargetReceipt = { kind: "runtime_target", version: 1, target: target.id,
36
- request: state.request, contract_event: state.contract_event, state: "unknown", reason: "runtime has not been observed",
37
- observed_at: new Date().toISOString(), source: { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(ctx.root, target.workdir ?? "."), runtime: `bun/${Bun.version}` },
38
- ...(target.port === undefined ? {} : { port: target.port }),
39
- checks: { build: false, inputs: false, artifacts: false, process: false, image: false, port: target.port === undefined ? null : false } };
40
- const raw: ArtifactRef[] = [];
41
- let processResult: ToolResult | undefined;
42
- let launched = false;
43
- try {
44
- const build = await this.build(ctx, history, state, target, receipt);
45
- const existing = this.live.get(target.id);
46
- if (input.action === "start") {
47
- if (input.session_id !== undefined) throw Error("start does not accept a session_id; inspect an existing handle instead");
48
- if (existing && findSession(existing.instance.session, ctx.execOwner)?.alive()) throw Error(`target already owns session ${existing.instance.session}; inspect or stop that handle before starting another`);
49
- if (!build) throw Error(receipt.reason);
50
- const cwd = await targetDirectory(ctx.root, target.workdir ?? ".");
51
- const entryPath = join(await targetDirectory(ctx.root, "."), target.entry);
52
- const program = await targetProgram(target.interpreter ?? entryPath, ctx.signal);
53
- if (ctx.signal?.aborted) throw Error("target start cancelled before execution");
54
- const argv = [program.path, ...(target.interpreter ? [entryPath] : []), ...(target.args ?? [])];
55
- const command = argv.map(arg => `'${arg.replaceAll("'", "'\\''")}'`).join(" ");
56
- const source = { ...receipt.source, cwd };
57
- const session = await startSession({ argv, command, cwd, tty: false, spoolDir: archive, source,
58
- timeoutMs: commandTimeout(input.timeout, ctx.timeoutMs, ctx.maxTimeoutMs), ...(ctx.execOwner ? { owner: ctx.execOwner } : {}) });
59
- if ("error" in session) throw Error(session.error);
60
- launched = true;
61
- const entry = build.outputs.find(output => output.path === target.entry)!.after;
62
- if (entry.state !== "observed") throw Error("target entry observation missing");
63
- const live: LiveTarget = { request: state.request, contract: state.contract_event, build: receipt.build!.event,
64
- instance: { id: randomUUID(), session: session.id, pid: session.pid, source, entry: { path: entryPath, sha256: entry.sha256 }, program } };
65
- this.live.set(target.id, live);
66
- const taken = await pollSession(session, archive, ctx.maxOutputBytes, yieldWait(input.yield_time_ms ?? 1000).ms, "", ctx.signal);
67
- raw.push(taken.stored.ref);
68
- processResult = await sessionResult(ctx, session.command, session.timeoutMs, taken);
69
- }
70
- const live = this.live.get(target.id);
71
- if (!live || live.request !== state.request || live.contract !== state.contract_event) throw Error("no live target capability in this Session; historical PID observations are not reattached");
72
- receipt.instance = live.instance; receipt.source = live.instance.source;
73
- if (input.session_id !== undefined && live.instance.session !== input.session_id) throw Error("requested handle is not this target instance");
74
- if (!build || live.build !== receipt.build?.event) { receipt.state = "stale"; throw Error("the running instance is not bound to the current successful build"); }
75
- const session = findSession(live.instance.session, ctx.execOwner);
76
- if (!session || !session.alive() || session.exit !== null) { receipt.state = "stale"; throw Error("owned runtime exited or its live capability was lost"); }
77
- const actual = await observeRuntimeIdentity(session.pid, live.instance.program, target.port, archive, ctx.signal);
78
- raw.push(...actual.raw);
79
- // Recheck after OS inspection: neither an exec nor a file replacement during a probe can inherit readiness.
80
- const current = await targetProgram(target.interpreter ?? live.instance.entry.path, ctx.signal);
81
- const currentBuild = await this.build(ctx, history, state, target, receipt);
82
- receipt.checks.process = findSession(session.id, ctx.execOwner) === session && session.alive() && session.exit === null;
83
- receipt.checks.image = actual.image && canonicalValue(current) === canonicalValue(live.instance.program);
84
- receipt.checks.port = actual.port;
85
- receipt.reason = !currentBuild ? receipt.reason : !receipt.checks.process ? "owned runtime exited during observation" :
86
- !receipt.checks.image ? "runtime executable identity changed or could not be established" : actual.reason;
87
- receipt.state = targetIsReady(receipt) ? "ready" : !actual.known ? "unknown" : !currentBuild || !receipt.checks.process ? "stale" : "not_ready";
88
- } catch (error) { receipt.reason = error instanceof Error ? error.message : "target observation failed"; }
89
- // Cancellation can arrive during a bounded file/OS probe, after the first
90
- // poll released its listener. Finish through the same owned process path.
91
- if (ctx.signal?.aborted) {
92
- const live = this.live.get(target.id);
93
- const session = live && live.request === state.request && live.contract === state.contract_event ? findSession(live.instance.session, ctx.execOwner) : undefined;
94
- if (session) {
95
- const taken = await pollSession(session, archive, ctx.maxOutputBytes, 0, "", ctx.signal);
96
- raw.push(taken.stored.ref); processResult = await sessionResult(ctx, session.command, session.timeoutMs, taken);
97
- }
98
- receipt.state = "unknown"; receipt.checks.process = false; receipt.reason = "target observation cancelled";
99
- }
100
- receipt.observed_at = new Date().toISOString();
101
- try { receipt.manifest = await archiveCommandArtifacts(archive, { observation: receipt, raw }); }
102
- catch { receipt.state = "unknown"; receipt.reason = "runtime observation archive unavailable"; }
103
- const content = `target ${target.id}: ${receipt.state}; ${receipt.reason}\n` +
104
- `build ${receipt.build?.event ?? "none"}; entry ${receipt.instance?.entry.path ?? target.entry}; ` +
105
- `session ${receipt.instance?.session ?? "none"}; pid ${receipt.instance?.pid ?? "unknown"}; port ${target.port ?? "not declared"}` +
106
- (receipt.manifest ? `\nObservation: history artifact=${receipt.manifest.sha256}` : "") +
107
- (receipt.state === "ready" ? "\nAwaiting explicit user acceptance of this instance." : "");
108
- const result: ToolResult = { ...processResult, ok: receipt.state === "ready", content: content + (processResult ? `\n${processResult.content}` : ""),
109
- // Process exit and target readiness are different operations. Keep the
110
- // actual exit in effect/data; only the target owns this tool's outcome.
111
- outcome: processResult?.outcome?.reason === "cancelled" || processResult?.outcome?.reason === "timeout" ? processResult.outcome :
112
- executionOutcome(ctx.signal?.aborted ? "cancelled" : receipt.state === "ready" ? "tool_completed" : "tool_failed"),
113
- ...(processResult?.data ? {} : { data: receipt }),
114
- ...(receipt.state === "ready" ? {} : { diagnostic: failureDiagnostic(ctx.signal?.aborted ? "cancelled" : "unknown_effect", receipt.reason, launched ? UNKNOWN_EFFECT : NOT_STARTED) }) };
115
- // The original stream reference must still match the process receipt. The
116
- // separate target event/manifest preserves the richer runtime observation.
117
- return { result, observation: receipt };
118
- }
119
-
120
- private async build(ctx: ToolContext, history: History, state: AcceptanceState, target: RuntimeTarget, receipt: RuntimeTargetReceipt): Promise<CommandArtifactReceipt | undefined> {
121
- const criterion = state.criteria.find(item => item.id === target.build);
122
- const part = history.find(part => part.events.some(event => event.id === criterion?.evidence?.event_id));
123
- const event = part?.events.find(event => event.id === criterion?.evidence?.event_id);
124
- const build = event?.t === "tool_result" && event.ok && event.effect?.kind === "command" && event.effect.exit === 0 ? event.effect.artifacts : undefined;
125
- if (criterion?.status !== "pass" || !isCommandArtifactReceipt(build) || build.state !== "observed" || !build.manifest || !part || !event ||
126
- criterion.kind !== "command" || build.command !== criterion.command || build.source.host !== receipt.source.host ||
127
- build.source.workspace !== resolve(ctx.root) || canonicalValue(build.plan) !== canonicalValue(target.artifacts)) {
128
- receipt.reason = `run the exact build condition ${target.build} with artifacts=${JSON.stringify(target.artifacts)} on this host and workspace`; return undefined;
129
- }
130
- receipt.build = { node: part.node, event: event.id, manifest: build.manifest };
131
- const exclusions = artifactExclusions(ctx.root, target.artifacts, [...(ctx.ownedStatePaths ?? []), ctx.outputStore ?? join(ctx.root, ".xeno", "observations")]);
132
- const { manifest, ...original } = build;
133
- if (manifest.bytes > MANIFEST_BYTES) throw Error("build manifest exceeds runtime verification budget");
134
- const page = await readObservation(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), manifest.sha256, 0, MANIFEST_BYTES);
135
- if (page.next !== null || page.bytes !== manifest.bytes || canonicalValue(JSON.parse(page.text)) !== canonicalValue(original) ||
136
- canonicalValue(exclusions) !== canonicalValue(build.inputs.exclusions)) throw Error("build observation scope or archived manifest does not match its original receipt");
137
- receipt.checks.build = true;
138
- const inputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
139
- const files = await observeArtifactFiles(ctx.root, target.artifacts.paths, ctx.signal);
140
- receipt.checks.inputs = inputs.sha256 !== null && inputs.sha256 === build.inputs.after;
141
- receipt.checks.artifacts = build.outputs.every((output, i) => {
142
- const file = files[i];
143
- return file?.state === "observed" && output.after.state === "observed" && file.sha256 === output.after.sha256 &&
144
- file.bytes === output.after.bytes && sameArtifactIdentity(file.identity, output.after.identity);
145
- });
146
- if (!receipt.checks.inputs || !receipt.checks.artifacts) { receipt.state = "stale"; receipt.reason = "build inputs or artifact bytes/identity changed; rebuild before launching or accepting"; return undefined; }
147
- return build;
148
- }
149
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * The session's live view of its secret vault (S.9). Rules and the inverse
3
- * live in core/secrets.ts; this owns the mutable entries, their durable
4
- * append, and the walk over an event's fields.
5
- *
6
- * One keeper per session tree: a branch shares its parent's, because both
7
- * write the same log and a placeholder number must mean one value in it.
8
- */
9
- import { expandText, isSolicitedSecret, mapStrings, placeholder, redactText, type SecretEntry } from "../core/secrets.ts";
10
- import type { LogEvent } from "../core/events.ts";
11
- import { appendVault, readVault } from "../store/secrets.ts";
12
-
13
- // Fields whose bytes a provider or the store verifies: rewriting them breaks
14
- // replay (signed thinking, raw provider blocks) or identity, and none of them
15
- // carries text a person typed or a tool printed.
16
- const VERIFIED_FIELDS = new Set(["id", "at", "raw", "wire", "signature", "replay_key", "sha256", "call_id", "user_id", "parent_call_id"]);
17
-
18
- export class SecretKeeper {
19
- private entries: SecretEntry[] = [];
20
- private pending: Promise<void> = Promise.resolve();
21
-
22
- private readonly dir: string;
23
- private readonly candidates: readonly { value: string; rule: string }[];
24
-
25
- constructor(dir: string, candidates: readonly { value: string; rule: string }[] = []) {
26
- this.dir = dir; this.candidates = candidates;
27
- }
28
-
29
- async load(): Promise<void> {
30
- const stored = await readVault(this.dir);
31
- // Entries this keeper already assigned (none before start) stay; stored ones fill in.
32
- for (const entry of stored) if (!this.entries.some(known => known.n === entry.n)) this.entries.push(entry);
33
- }
34
-
35
- /**
36
- * Returns the event to write and whether this call added vault entries. The
37
- * entries are in memory at once -- the next event numbers after them -- and
38
- * durable before the returned promise resolves, so no written event ever
39
- * holds a placeholder the vault cannot expand.
40
- */
41
- async redact(event: LogEvent, previousAssistantText: string | undefined): Promise<{ event: LogEvent; added: number }> {
42
- const added: SecretEntry[] = [];
43
- const redact = (text: string): string => {
44
- const result = redactText(text, [...this.entries, ...added], this.candidates);
45
- added.push(...result.added);
46
- return result.text;
47
- };
48
- const skip = (key: string) => VERIFIED_FIELDS.has(key);
49
- let next: LogEvent;
50
- if (event.t === "checkpoint") next = event; // file fingerprints and paths, never projected
51
- else if (event.t === "assistant") {
52
- // Thinking is replayed under the provider's signature; a changed byte is a rejected request.
53
- next = { ...event, blocks: event.blocks.map(block => block.type === "thinking" ? block : mapStrings(block, redact, skip) as typeof block) };
54
- } else if (event.t === "user" && isSolicitedSecret(event.text, previousAssistantText)) {
55
- // The whole message is the credential, however short: it was asked for.
56
- const value = event.text.trim();
57
- const entry = this.entries.find(known => known.value === value) ??
58
- { n: this.entries.reduce((max, known) => Math.max(max, known.n), 0) + 1, value, rule: "solicited" };
59
- if (!this.entries.includes(entry)) added.push(entry);
60
- next = { ...mapStrings(event, redact, key => skip(key) || key === "text") as LogEvent & { t: "user" }, text: placeholder(entry.n) };
61
- } else next = mapStrings(event, redact, skip) as LogEvent;
62
- if (added.length > 0) {
63
- this.entries.push(...added);
64
- const write = this.pending.then(() => appendVault(this.dir, added));
65
- this.pending = write.catch(() => {});
66
- await write;
67
- }
68
- return { event: next, added: added.length };
69
- }
70
-
71
- /** Tool input as the tool should run it: placeholders become values. */
72
- expand(input: unknown): unknown {
73
- return this.entries.length === 0 ? input : mapStrings(input, text => expandText(text, this.entries));
74
- }
75
- }
76
-
77
- export const SECRET_GUIDANCE = "Values written as [[xeno-secret-N]] are credentials xeno hid from you. " +
78
- "Use the placeholder exactly as written in tool inputs; xeno substitutes the real value when the tool runs " +
79
- "and hides it again in the output. Do not ask the user to repeat a hidden value.";
@@ -1,74 +0,0 @@
1
- /**
2
- * Run the package manager for an explicit `/update` (docs/tasks/fix-update-installs.md).
3
- *
4
- * The argv comes from core's `installArgv`; this module only owns the process.
5
- * Every competitor runs the install unbounded, and that is exactly what their
6
- * hang reports are about (claude-code #80147, opencode #10921 `bun add`
7
- * stuck forever), so the install has a deadline and streams its latest line
8
- * to the caller as evidence that it is moving.
9
- */
10
- import { access, constants } from "node:fs/promises";
11
- import type { InstallOutcome } from "../core/update-check.ts";
12
-
13
- /** A global bun/npm install normally takes seconds; three minutes covers a slow registry. Assumed. */
14
- export const INSTALL_TIMEOUT_MS = 180_000;
15
- /** Lines of package-manager output kept for a failure message: enough for an npm error block. */
16
- const TAIL_LINES = 6;
17
-
18
- export interface InstallOptions {
19
- readonly signal?: AbortSignal;
20
- readonly timeoutMs?: number;
21
- /** The package manager's newest non-empty output line. */
22
- readonly onLine?: (line: string) => void;
23
- /** Refuse before spawning when this directory cannot be written, as pi does. */
24
- readonly writableDir?: string;
25
- }
26
-
27
- const ANSI = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07|\r/g;
28
-
29
- export async function runInstall(argv: readonly string[], options: InstallOptions = {}): Promise<InstallOutcome> {
30
- if (options.signal?.aborted) return { kind: "cancelled" };
31
- if (options.writableDir) {
32
- try { await access(options.writableDir, constants.W_OK); }
33
- catch { return { kind: "unavailable", reason: `${options.writableDir} is not writable by this user` }; }
34
- }
35
- // Resolve through PATH like the person's own shell would; a bun-run xeno can always fall back to its own bun.
36
- const program = Bun.which(argv[0]!) ?? (argv[0] === "bun" ? process.execPath : null);
37
- if (!program) return { kind: "unavailable", reason: `${argv[0]} was not found on PATH` };
38
- let child: ReturnType<typeof Bun.spawn<"ignore", "pipe", "pipe">>;
39
- try { child = Bun.spawn([program, ...argv.slice(1)], { stdin: "ignore", stdout: "pipe", stderr: "pipe" }); }
40
- catch (error) { return { kind: "unavailable", reason: error instanceof Error ? error.message : String(error) }; }
41
-
42
- const output: string[] = [];
43
- const read = async (stream: ReadableStream<Uint8Array>) => {
44
- const decoder = new TextDecoder();
45
- let pending = "";
46
- const take = (line: string) => {
47
- const clean = line.replace(ANSI, "").trim();
48
- if (!clean) return;
49
- output.push(clean); if (output.length > TAIL_LINES) output.shift();
50
- options.onLine?.(clean);
51
- };
52
- for await (const chunk of stream) {
53
- const lines = (pending + decoder.decode(chunk, { stream: true })).split("\n");
54
- pending = lines.pop()!;
55
- lines.forEach(take);
56
- }
57
- take(pending + decoder.decode());
58
- };
59
-
60
- let stopped: "timeout" | "cancelled" | undefined;
61
- const timeoutMs = options.timeoutMs ?? INSTALL_TIMEOUT_MS;
62
- const timer = setTimeout(() => { stopped = "timeout"; child.kill(); }, timeoutMs);
63
- const abort = () => { stopped ??= "cancelled"; child.kill(); };
64
- options.signal?.addEventListener("abort", abort, { once: true });
65
- try {
66
- const [code] = await Promise.all([child.exited, read(child.stdout), read(child.stderr)]);
67
- if (stopped === "timeout") return { kind: "timeout", ms: timeoutMs, output };
68
- if (stopped === "cancelled") return { kind: "cancelled" };
69
- return code === 0 ? { kind: "installed" } : { kind: "exit", code, output };
70
- } finally {
71
- clearTimeout(timer);
72
- options.signal?.removeEventListener("abort", abort);
73
- }
74
- }
@@ -1,265 +0,0 @@
1
- /** Q24: one controller per live Session, shared by the TUI and structured adapters. */
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { resolve } from "node:path";
4
- import { goalView, contextLens, type GoalView, type ContextLens } from "../core/control-view.ts";
5
- import { DEFAULT_CONTEXT_POLICY } from "../core/context-policy.ts";
6
- import { userInputData, type UserInput } from "../core/images.ts";
7
- import type { Phase } from "../core/queue.ts";
8
- import type { PermissionRequest } from "../core/permissions.ts";
9
- import type { ExecutionOutcome } from "../core/outcome.ts";
10
- import type { LogEvent } from "../core/events.ts";
11
- import { readNodeHistory } from "../store/restore.ts";
12
- import type { Session, LoopEvent, RequestOptions } from "./loop.ts";
13
- import type { ApprovalChoice } from "./tools.ts";
14
- import { validateQuestionReply, type QuestionRequest, type QuestionReply } from "../core/question.ts";
15
- import type { QuestionSlot } from "./question-tool.ts";
16
- import { requestExternalEffects } from "./turn-effects.ts";
17
- import type { ExternalEffect } from "../core/external-effects.ts";
18
-
19
- export type ControlNotice = { method: string; params: { threadId: string; streamId: string; sequence: number; turnId?: string; [key: string]: unknown } };
20
- export type PendingApproval = { id: string; turnId: string; request: PermissionRequest };
21
- export type ControlSnapshot = {
22
- version: "xeno-control/0.1"; threadId: string; streamId: string; sequence: number; node: number;
23
- activeTurnId: string | null; goal: GoalView | null; context: ContextLens;
24
- workflowMode: "build" | "plan";
25
- waiting: "permission" | "question" | "auth" | null; approvals: PendingApproval[]; questions: QuestionRequest[];
26
- };
27
-
28
- export class SessionControl {
29
- /** App-owned configuration switches resources at an idle checkpoint; observers cannot mutate it. */
30
- configureWorkflow?: (mode: "build" | "plan") => Promise<void>;
31
- readonly questionSlot: QuestionSlot;
32
- private questionClients = 0;
33
- private readonly questionAdapter: NonNullable<QuestionSlot["current"]> = (request, signal) => this.askQuestion(request, signal);
34
- private readonly questions = new Map<string, { request: QuestionRequest; resolve(reply: QuestionReply | undefined): void }>();
35
- readonly session: Session;
36
- readonly threadId: string;
37
- readonly streamId = randomUUID();
38
- private sequence = 0;
39
- private listeners = new Set<(event: ControlNotice) => void>();
40
- private active: { id: string; abort: AbortController; queue: UserInput[] } | null = null;
41
- private task: Promise<void> | null = null;
42
- private approvals = new Map<string, { data: PendingApproval; resolve: (choice: ApprovalChoice) => void }>();
43
- private waiting: ControlSnapshot["waiting"] = null;
44
- private changing = false;
45
- private inputSource: ((phase: Phase) => Promise<UserInput | null>) | undefined;
46
- /** What the last finished turn did outside the workspace, as sent with its turn/completed. */
47
- lastExternalEffects: readonly ExternalEffect[] = [];
48
-
49
- constructor(session: Session) {
50
- this.session = session;
51
- this.questionSlot = session.config.tools.questionSlot ?? {};
52
- // Same identity rule as provider affinity; no new durable identity store (D8).
53
- this.threadId = createHash("sha256").update(resolve(session.config.logDir)).digest("hex");
54
- session.subscribeRecords(item => this.emit("item/completed", { item }));
55
- }
56
- get activeTurnId(): string | null { return this.active?.id ?? null; }
57
- get busy(): boolean { return this.changing || this.active !== null || this.session.running; }
58
-
59
- /** One owner excludes turn admission across asynchronous checkpoint/configuration work. */
60
- async withIdleChange<T>(operation: () => Promise<T>): Promise<T> {
61
- if (this.busy) throw Error("configuration changes require an idle session");
62
- this.changing = true;
63
- try { const result = await operation(); this.emit("thread/statusChanged"); return result; }
64
- finally { this.changing = false; }
65
- }
66
-
67
- subscribe(listener: (event: ControlNotice) => void): () => void {
68
- this.listeners.add(listener); return () => { this.listeners.delete(listener); };
69
- }
70
- /** The attached editor owns retractable drafts even when an adapter starts the turn. */
71
- attachInput(pull: (phase: Phase) => Promise<UserInput | null>): () => void {
72
- if (this.inputSource) throw Error("the session already has an attached input owner");
73
- this.inputSource = pull;
74
- return () => { if (this.inputSource === pull) this.inputSource = undefined; };
75
- }
76
- private emit(method: string, data: Record<string, unknown> = {}): void {
77
- const event: ControlNotice = { method, params: { ...data, threadId: this.threadId, streamId: this.streamId,
78
- sequence: ++this.sequence, ...(this.active ? { turnId: this.active.id } : {}) } };
79
- for (const listener of this.listeners) {
80
- // Observers are outside the evidence owner: mutations and failures cannot alter execution.
81
- try { listener(structuredClone(event)); } catch { /* the transport owns delivery errors */ }
82
- }
83
- }
84
- async snapshot(observe = true): Promise<ControlSnapshot> {
85
- if (observe && !this.busy) await this.session.acceptanceState();
86
- const node = this.session.state.node;
87
- const history = await readNodeHistory(this.session.config.logDir, node, undefined, "context");
88
- if (!history.ok) throw Error(history.error);
89
- if (node !== this.session.state.node) throw Error("session changed while reading; refresh the snapshot");
90
- const s = this.session;
91
- // Ancestors are sealed. The live segment must share the cursor's publication
92
- // boundary: a disk read can finish between append and in-memory publication.
93
- const parts = history.nodes.map(part => part.node === node ? { node, events: structuredClone(s.state.events) } : part);
94
- const projectedGoal = goalView(parts, { strict: s.config.strictVerification === true, blind: s.config.blindChecks !== undefined });
95
- const goal = this.active && projectedGoal?.state.request.event !== this.active.id ? null : projectedGoal;
96
- return { version: "xeno-control/0.1", threadId: this.threadId, streamId: this.streamId, sequence: this.sequence, node,
97
- activeTurnId: this.activeTurnId, goal, waiting: this.waiting, workflowMode: s.config.tools.workflowMode ?? "build",
98
- approvals: structuredClone([...this.approvals.values()].map(item => item.data)),
99
- questions: structuredClone([...this.questions.values()].map(item => item.request)),
100
- context: contextLens({ ...s.contextInput(), anchor: s.state.anchor, policy: s.config.contextPolicy ?? DEFAULT_CONTEXT_POLICY,
101
- contextWindow: s.provider.config.spec.contextWindow, outputReserve: s.provider.config.maxTokens,
102
- ...(s.config.counter ? { counter: s.config.counter } : {}), ...(s.config.boundary ? { boundary: s.config.boundary } : {}) }) };
103
- }
104
- async items(): Promise<{ node: number; event: LogEvent }[]> {
105
- const node = this.session.state.node;
106
- const history = await readNodeHistory(this.session.config.logDir, node, undefined, "context");
107
- if (!history.ok) throw Error(history.error);
108
- if (node !== this.session.state.node) throw Error("session changed while reading; refresh the snapshot");
109
- return history.nodes.flatMap(part => (part.node === node ? structuredClone(this.session.state.events) : part.events)
110
- .map(event => ({ node: part.node, event })));
111
- }
112
-
113
- /** Both local and remote turns pass through this iterator; Session remains the execution owner. */
114
- async *send(input: UserInput, signal?: AbortSignal, pull?: (phase: Phase) => Promise<UserInput | null>, options?: RequestOptions): AsyncGenerator<LoopEvent> {
115
- if (this.busy) throw Error("a turn is already running");
116
- const active = { id: this.session.clock.nextId(), abort: new AbortController(), queue: [] as UserInput[] };
117
- this.active = active;
118
- this.questionSlot.activeTurn = active.id;
119
- const signals = signal ? AbortSignal.any([signal, active.abort.signal]) : active.abort.signal;
120
- let outcome: ExecutionOutcome | undefined;
121
- try {
122
- this.emit("turn/started");
123
- const steering = async (phase: Phase): Promise<UserInput | null> => {
124
- const external = active.queue.splice(0), local = await (pull ?? this.inputSource)?.(phase);
125
- if (local) external.push(local);
126
- const data = external.map(value => userInputData(value));
127
- const text = data.map(value => value.text).join("\n\n"), images = data.flatMap(value => value.images ?? []);
128
- return images.length ? { text, images } : text || null;
129
- };
130
- for await (const event of this.session.send(input, signals, steering, active.id, options)) {
131
- this.emit("turn/event", { event });
132
- if (event.kind === "done") outcome = event.outcome;
133
- yield event;
134
- }
135
- } catch (error) {
136
- // Raw provider errors may contain credentials. The owner can report a sanitized error separately.
137
- this.emit("turn/error", { message: "turn failed; inspect the session outcome" });
138
- throw error;
139
- } finally {
140
- for (const approval of [...this.approvals.values()]) approval.resolve("deny");
141
- for (const question of [...this.questions.values()]) question.resolve(undefined);
142
- delete this.questionSlot.activeTurn;
143
- this.waiting = null;
144
- // A thrown provider error also appends an outcome. Clients must be able to discover it.
145
- try {
146
- if (!outcome) {
147
- const state = (await this.snapshot(false)).goal?.state;
148
- if (state?.request.event === active.id) outcome = state.execution;
149
- }
150
- } catch { /* unreadable storage stays unknown, never an invented completion */ }
151
- // feat-external-effects-summary: a projection of this request's receipts, never a stored event.
152
- let externalEffects: ExternalEffect[] = [];
153
- try { externalEffects = requestExternalEffects(this.session); } catch { /* the list is advisory; a read failure shows none */ }
154
- this.lastExternalEffects = externalEffects;
155
- this.emit("turn/completed", { ...(outcome ? { outcome } : {}), steeringUndelivered: active.queue.length, externalEffects });
156
- this.active = null;
157
- }
158
- }
159
- start(input: UserInput): string {
160
- if (this.busy) throw Error("a turn is already running");
161
- this.task = (async () => { for await (const _event of this.send(input)) { /* notices are the client stream */ } })();
162
- void this.task.catch(() => {});
163
- return this.active!.id;
164
- }
165
- async whenIdle(): Promise<void> { await this.task; await this.session.whenIdle(); }
166
- private current(turnId: string): NonNullable<SessionControl["active"]> {
167
- if (!this.active || this.active.id !== turnId) throw Error("stale or unknown turnId");
168
- return this.active;
169
- }
170
- steer(turnId: string, input: UserInput): void {
171
- const turn = this.current(turnId);
172
- // Assumed ingress bound, not an execution policy. Keep a client from queuing unbounded work.
173
- if (turn.queue.length >= 32) throw Error("steering queue is full");
174
- turn.queue.push(structuredClone(input));
175
- this.session.wakeProcessWaits();
176
- this.emit("turn/steeringQueued", { count: turn.queue.length });
177
- }
178
- interrupt(turnId: string): void {
179
- this.current(turnId).abort.abort();
180
- for (const approval of [...this.approvals.values()]) approval.resolve("deny");
181
- for (const question of [...this.questions.values()]) question.resolve(undefined);
182
- this.session.cancel();
183
- }
184
- setWaiting(kind: ControlSnapshot["waiting"]): void {
185
- this.waiting = this.approvals.size ? "permission" : kind;
186
- this.emit("thread/statusChanged", { waiting: this.waiting });
187
- }
188
- ask(request: PermissionRequest, present?: (id: string, resolve: (choice: ApprovalChoice) => void) => (() => void)): Promise<ApprovalChoice> {
189
- if (!this.active) return Promise.resolve("deny");
190
- const data = { id: randomUUID(), turnId: this.active.id, request: structuredClone(request) };
191
- return new Promise(resolveChoice => {
192
- let close: (() => void) | undefined;
193
- const resolveOnce = (choice: ApprovalChoice) => {
194
- if (!this.approvals.delete(data.id)) return;
195
- close?.(); this.waiting = this.approvals.size ? "permission" : null;
196
- this.emit("approval/resolved", { approvalId: data.id }); resolveChoice(choice);
197
- };
198
- this.approvals.set(data.id, { data, resolve: resolveOnce }); this.waiting = "permission";
199
- this.emit("approval/request", { approval: data });
200
- if (this.approvals.has(data.id)) {
201
- try { close = present?.(data.id, resolveOnce); }
202
- catch { resolveOnce("deny"); }
203
- }
204
- });
205
- }
206
- respond(turnId: string, id: string, choice: ApprovalChoice): void {
207
- this.current(turnId);
208
- const approval = this.approvals.get(id);
209
- if (!approval || approval.data.turnId !== turnId) throw Error("stale or unknown approvalId");
210
- approval.resolve(choice);
211
- }
212
- denyApprovals(): void { for (const approval of [...this.approvals.values()]) approval.resolve("deny"); }
213
- attachQuestionClient(): () => void {
214
- this.questionClients++;
215
- if (!this.questionSlot.current) this.questionSlot.current = this.questionAdapter;
216
- let closed = false;
217
- return () => {
218
- if (closed) return; closed = true;
219
- if (--this.questionClients === 0) {
220
- if (this.questionSlot.current === this.questionAdapter) delete this.questionSlot.current;
221
- for (const question of [...this.questions.values()]) question.resolve(undefined);
222
- }
223
- };
224
- }
225
- askQuestion(request: QuestionRequest, signal: AbortSignal, present?: (request: QuestionRequest) => Promise<unknown>): Promise<QuestionReply | undefined> {
226
- if (!this.active || request.turn_id !== this.active.id || signal.aborted || !present && this.questionClients === 0) return Promise.resolve(undefined);
227
- if (this.questions.has(request.request_id)) return Promise.reject(Error("duplicate question invocation"));
228
- return new Promise(resolve => {
229
- const finish = (reply: QuestionReply | undefined) => {
230
- if (!this.questions.delete(request.request_id)) return;
231
- signal.removeEventListener("abort", aborted); this.setWaiting(this.questions.size ? "question" : null);
232
- this.emit("question/resolved", { requestId: request.request_id }); resolve(reply);
233
- };
234
- const aborted = () => finish(undefined);
235
- this.questions.set(request.request_id, { request: structuredClone(request), resolve: finish });
236
- signal.addEventListener("abort", aborted, { once: true }); this.setWaiting("question");
237
- this.emit("question/request", { question: structuredClone(request) });
238
- if (present) void present(structuredClone(request)).then(value => {
239
- if (value === undefined) finish(undefined); else try { finish(validateQuestionReply(request, value)); } catch { finish(undefined); }
240
- }).catch(() => finish(undefined));
241
- });
242
- }
243
- respondQuestion(turnId: string, requestId: string, reply: unknown): void {
244
- this.current(turnId); const question = this.questions.get(requestId);
245
- if (!question || question.request.turn_id !== turnId) throw Error("stale or unknown question requestId");
246
- question.resolve(validateQuestionReply(question.request, reply));
247
- }
248
- async accept(requestId: string, criterion: string): Promise<void> {
249
- if (this.busy) throw Error("acceptance requires an idle session");
250
- this.changing = true;
251
- try {
252
- const state = await this.session.requestState();
253
- if (state?.request.event !== requestId) throw Error("stale or unknown requestId");
254
- await this.session.acceptCriterion(criterion);
255
- this.emit("thread/statusChanged");
256
- } finally { this.changing = false; }
257
- }
258
- }
259
-
260
- const controls = new WeakMap<Session, SessionControl>();
261
- export function sessionControl(session: Session): SessionControl {
262
- let control = controls.get(session);
263
- if (!control) { control = new SessionControl(session); controls.set(session, control); }
264
- return control;
265
- }