@visual-z/xeno 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `/model` list order. Pure: the catalog, readiness and recency go in,
3
+ * rows come out.
4
+ *
5
+ * The catalog holds thousands of entries across hundreds of providers; a
6
+ * picker that lists them all is the "every time I pick a model it's hard"
7
+ * report. The rule is OpenCode's (dialog-model.tsx): only models whose
8
+ * provider can actually authenticate, Recent first (bounded), then one
9
+ * group per provider, in catalog order. The current model is always shown, even when its
10
+ * credential stopped resolving, so the user can see why they are stuck.
11
+ */
12
+ export interface PickerModel {
13
+ readonly key: string;
14
+ readonly provider: string;
15
+ readonly providerName: string;
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly ready: boolean;
19
+ }
20
+
21
+ export interface PickerRow {
22
+ readonly key: string;
23
+ readonly section: "recent" | "provider";
24
+ /** Group heading: "Recent" or the provider's display name. */
25
+ readonly group: string;
26
+ readonly model: PickerModel;
27
+ readonly current: boolean;
28
+ }
29
+
30
+ /** Five recent models: one screen of a 10-row list stays for the provider groups. Assumed. */
31
+ export const RECENT_LIMIT = 5;
32
+
33
+ export function pickerRows(models: readonly PickerModel[], opts: { readonly current: string; readonly recent: readonly string[]; readonly limit?: number }): PickerRow[] {
34
+ const visible = models.filter(model => model.ready || model.key === opts.current);
35
+ const byKey = new Map(visible.map(model => [model.key, model]));
36
+ const rows: PickerRow[] = [];
37
+ const used = new Set<string>();
38
+ for (const key of opts.recent) {
39
+ if (rows.length >= (opts.limit ?? RECENT_LIMIT)) break;
40
+ const model = byKey.get(key);
41
+ if (!model || used.has(key)) continue;
42
+ used.add(key);
43
+ rows.push({ key, section: "recent", group: "Recent", model, current: key === opts.current });
44
+ }
45
+ const groups = new Map<string, PickerModel[]>();
46
+ for (const model of visible) {
47
+ if (used.has(model.key)) continue;
48
+ used.add(model.key);
49
+ const list = groups.get(model.provider) ?? [];
50
+ list.push(model);
51
+ groups.set(model.provider, list);
52
+ }
53
+ // Catalog order is curated (flagship models first), so it is kept for
54
+ // both the groups and the models inside them instead of re-sorting.
55
+ for (const [, list] of groups) {
56
+ for (const model of list) rows.push({ key: model.key, section: "provider", group: model.providerName, model, current: model.key === opts.current });
57
+ }
58
+ return rows;
59
+ }
60
+
61
+ /** Most recent first, deduplicated, bounded. Recording is a pure update of the stored list. */
62
+ export function recordRecent(recent: readonly string[], key: string, limit = 10): string[] {
63
+ return [key, ...recent.filter(entry => entry !== key)].slice(0, limit);
64
+ }
@@ -0,0 +1,25 @@
1
+ /** Q.20: native subscription measurements are standard API price equivalents, never invoices. */
2
+ import { measuredCharge, requestReservation, type BenchmarkPrice, type BenchmarkUsage } from "./benchmark.ts";
3
+
4
+ // Verified Luna short-context tier. Bound wire bytes conservatively before dispatch;
5
+ // crossing this tier needs a new protocol, not silent use of the cheaper price.
6
+ export const NATIVE_EVAL_INPUT_LIMIT = 272000;
7
+ export function nativeUsage(response: unknown): BenchmarkUsage | null {
8
+ if (!response || typeof response !== "object") return null;
9
+ const raw = (response as Record<string, unknown>)["usage"];
10
+ if (!raw || typeof raw !== "object") return null;
11
+ const u = raw as Record<string, unknown>, details = u["input_tokens_details"] as Record<string, unknown> | undefined;
12
+ const input = u["input_tokens"], output = u["output_tokens"], cached = details?.["cached_tokens"] ?? 0, created = details?.["cache_write_tokens"] ?? 0;
13
+ if (![input, output, cached, created].every(n => typeof n === "number" && Number.isSafeInteger(n) && n >= 0) ||
14
+ Number(input) <= 0 || Number(output) <= 0 || Number(input) > NATIVE_EVAL_INPUT_LIMIT || Number(cached) + Number(created) > Number(input)) return null;
15
+ return { input_tokens: Number(input) - Number(cached) - Number(created), output_tokens: Number(output),
16
+ cache_read_input_tokens: Number(cached), cache_creation_input_tokens: Number(created) };
17
+ }
18
+ export function nativeReservation(bytes: number, modelMaxOutput: number, price: BenchmarkPrice): number {
19
+ if (bytes + 4096 > NATIVE_EVAL_INPUT_LIMIT || price.cacheWrite === undefined) throw Error("native eval price tier or cache-write rate is not covered");
20
+ return requestReservation(bytes, modelMaxOutput, { ...price, input: Math.max(price.input, price.cacheWrite) });
21
+ }
22
+ export function nativeMeasured(response: unknown, price: BenchmarkPrice): number | null {
23
+ const usage = nativeUsage(response);
24
+ return usage && price.cacheWrite !== undefined ? measuredCharge(usage, price) : null;
25
+ }
@@ -0,0 +1,34 @@
1
+ /** Q.22: recovery advice must use the same evidence rules as the handoff. */
2
+ import type { LogEvent } from "./events.ts";
3
+ import { resolveEvidence, type EvidenceResolution } from "./evidence.ts";
4
+ import type { ToolDiagnostic } from "./tool-contract.ts";
5
+ import { failureDiagnostic, NOT_STARTED } from "./tool-failure.ts";
6
+
7
+ type Claim = Extract<LogEvent, { t: "claim" }>;
8
+
9
+ /** Only unknown work may suggest the existing unscoped completion path. */
10
+ export function unknownWorkClaimDiagnostic(events: readonly LogEvent[], claim: Claim): ToolDiagnostic {
11
+ const proof = resolveEvidence(events, claim.evidence);
12
+ const message = `unknown work_id ${claim.work_id}; no work item was created or completed`;
13
+ const diagnostic = failureDiagnostic("invalid_input", message, NOT_STARTED);
14
+ if (!proof.ok) return { ...diagnostic,
15
+ next: "For tracked work, record next before doing and verifying it. For a one-off task, omit work_id from done after an actual successful verification. A done note is optional." };
16
+ return { ...diagnostic,
17
+ next: "A current successful verification already exists. For this one-off completion, omit work_id; do not create next after verification or rerun the command just to record a note.",
18
+ recover: { tool: "note", input: { kind: "done", what: claim.what, proof: proof.evidence.command } } };
19
+ }
20
+
21
+ /** A persisted claim is a known state change, even when its proof is insufficient. */
22
+ export function unverifiedNoteDiagnostic(reason: Extract<EvidenceResolution, { ok: false }>, event: string, openedAt?: string): ToolDiagnostic {
23
+ const cause = reason.reason === "nonzero_exit" ? `the matching command exited ${reason.exit}`
24
+ : reason.reason === "stale_workspace" ? "the verification no longer matches the observed workspace"
25
+ : openedAt ? `no matching verification was observed after work activation ${openedAt} in this segment`
26
+ : "no matching verification was observed in this segment";
27
+ const message = `Claim ${event} was recorded but remains unverified: ${cause}.`;
28
+ return { ...failureDiagnostic("execution_failed", message, { execution: "started", effects: "possible" }),
29
+ next: "The claim is already recorded; do not replay it unchanged. " + (reason.reason === "nonzero_exit"
30
+ ? "Inspect the command's original output, fix the cause and verify again before claiming completion."
31
+ : reason.reason === "stale_workspace" ? "Verify the current workspace before claiming completion."
32
+ : openedAt ? "Run the verifier for this work activation before claiming completion; reopening never reuses an older verification."
33
+ : "Cite an actual successful command from this segment, or run the required verification first.") };
34
+ }
@@ -0,0 +1,44 @@
1
+ /** Validated wire data only; the runtime owns HTTP, time, polling and persistence. */
2
+ export interface OAuthTokens { access: string; refresh: string; expiresIn: number }
3
+ export interface DeviceAuthorization {
4
+ deviceCode: string;
5
+ userCode: string;
6
+ verificationUri: string;
7
+ verificationUriComplete?: string;
8
+ expiresIn: number;
9
+ interval?: number;
10
+ }
11
+
12
+ function record(value: unknown): Record<string, unknown> | undefined {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
14
+ }
15
+ function positive(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; }
16
+ function token(value: unknown): value is string { return typeof value === "string" && value.length > 0 && !/[\s\x00-\x1f\x7f]/.test(value); }
17
+
18
+ /** Refresh rotation is optional; omission preserves the existing refresh capability. */
19
+ export function parseOAuthTokens(value: unknown, previousRefresh?: string): OAuthTokens | undefined {
20
+ const data = record(value);
21
+ const access = data?.["access_token"], refresh = data?.["refresh_token"] ?? previousRefresh, expiresIn = data?.["expires_in"];
22
+ if (!token(access) || !token(refresh) || !positive(expiresIn)) return undefined;
23
+ return { access, refresh, expiresIn };
24
+ }
25
+
26
+ export function trustedOAuthUrl(value: unknown, origins: readonly string[]): value is string {
27
+ if (typeof value !== "string" || /[\x00-\x20\x7f]/.test(value)) return false;
28
+ try {
29
+ const url = new URL(value);
30
+ return url.protocol === "https:" && !url.username && !url.password && origins.includes(url.origin);
31
+ } catch { return false; }
32
+ }
33
+
34
+ /** Never open an issuer-supplied arbitrary origin or print its control characters. */
35
+ export function parseDeviceAuthorization(value: unknown, origins: readonly string[]): DeviceAuthorization | undefined {
36
+ const data = record(value);
37
+ const deviceCode = data?.["device_code"], userCode = data?.["user_code"], verificationUri = data?.["verification_uri"];
38
+ const complete = data?.["verification_uri_complete"], expiresIn = data?.["expires_in"], interval = data?.["interval"];
39
+ if (!token(deviceCode) || typeof userCode !== "string" || !/^[A-Za-z0-9-]+$/.test(userCode)
40
+ || !trustedOAuthUrl(verificationUri, origins) || !positive(expiresIn)
41
+ || (complete !== undefined && !trustedOAuthUrl(complete, origins)) || (interval !== undefined && !positive(interval))) return undefined;
42
+ return { deviceCode, userCode, verificationUri, expiresIn,
43
+ ...(typeof complete === "string" ? { verificationUriComplete: complete } : {}), ...(typeof interval === "number" ? { interval } : {}) };
44
+ }
@@ -7,6 +7,30 @@
7
7
  * failing build or test run prints last: the error and the summary. Codex
8
8
  * keeps half of each end (`utils/string/src/truncate.rs`); so does this.
9
9
  */
10
+ import type { ArtifactRef } from "./events.ts";
11
+
12
+ export interface BytePreview {
13
+ unit: "utf8_bytes";
14
+ complete: boolean;
15
+ visible: readonly (readonly [number, number])[];
16
+ next_offset: number | null;
17
+ }
18
+ const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
19
+ const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
20
+ export function isObservationArtifact(value: unknown): value is ArtifactRef {
21
+ return object(value) && typeof value["sha256"] === "string" && /^[a-f0-9]{64}$/.test(value["sha256"]) && typeof value["path"] === "string" && natural(value["bytes"]);
22
+ }
23
+ /** The same coverage invariant serves file reads and search projections. */
24
+ export function isBytePreview(value: unknown, total: number): value is BytePreview {
25
+ if (!object(value) || value["unit"] !== "utf8_bytes" || typeof value["complete"] !== "boolean" || !Array.isArray(value["visible"])) return false;
26
+ let last = 0, prefix = 0;
27
+ for (const range of value["visible"]) {
28
+ if (!Array.isArray(range) || range.length !== 2 || !natural(range[0]) || !natural(range[1]) || range[0] < last || range[1] <= range[0] || range[1] > total) return false;
29
+ if (prefix === range[0]) prefix = range[1];
30
+ last = range[1];
31
+ }
32
+ return value["complete"] === (prefix === total) && value["next_offset"] === (value["complete"] ? null : prefix);
33
+ }
10
34
 
11
35
  /** A UTF-8 continuation byte: cutting before one would split a character. */
12
36
  const continuation = (byte: number | undefined) => byte !== undefined && (byte & 0xc0) === 0x80;
@@ -29,20 +53,36 @@ export function headTailView(
29
53
  limit: number,
30
54
  marker: (omitted: number, offset: number) => string,
31
55
  ): string {
56
+ // Legacy command callers require the recovery locator even below its minimum display budget.
57
+ return headTailSelection(head, tail, total, limit, marker, true).text;
58
+ }
59
+
60
+ /** Half-open byte ranges in the original observation, excluding presentation markers. */
61
+ export function headTailSelection(
62
+ head: Uint8Array, tail: Uint8Array, total: number, limit: number,
63
+ marker: (omitted: number, offset: number) => string, allowOversizeMarker = false,
64
+ ): { text: string; visible: [number, number][] } {
32
65
  const decoder = new TextDecoder();
33
- if (total <= limit && head.length >= total) return decoder.decode(head.subarray(0, total));
66
+ if (total <= limit && head.length >= total) return { text: decoder.decode(head.subarray(0, total)), visible: total ? [[0, total]] : [] };
34
67
  // Digits of the real numbers never exceed digits of `total`, so this bounds
35
68
  // the marker before the cut points that decide its numbers are known.
36
69
  const reserve = new TextEncoder().encode(marker(total, total)).length;
70
+ if (reserve > limit && !allowOversizeMarker) {
71
+ const bytes = new TextEncoder().encode(marker(total, 0));
72
+ let end = Math.max(0, Math.min(Math.floor(limit), bytes.length));
73
+ while (end > 0 && continuation(bytes[end])) end--;
74
+ return { text: decoder.decode(bytes.subarray(0, end)), visible: [] };
75
+ }
37
76
  const budget = Math.max(0, limit - reserve);
38
77
  let headEnd = Math.min(Math.floor(budget / 2), head.length);
39
78
  while (headEnd > 0 && continuation(head[headEnd])) headEnd--;
40
79
  let tailStart = tail.length - Math.min(budget - headEnd, tail.length);
41
80
  while (tailStart < tail.length && continuation(tail[tailStart])) tailStart++;
42
81
  const kept = tail.length - tailStart;
43
- return decoder.decode(head.subarray(0, headEnd)) +
82
+ return { text: decoder.decode(head.subarray(0, headEnd)) +
44
83
  marker(total - headEnd - kept, headEnd) +
45
- decoder.decode(tail.subarray(tailStart));
84
+ decoder.decode(tail.subarray(tailStart)),
85
+ visible: [...(headEnd ? [[0, headEnd] as [number, number]] : []), ...(kept ? [[total - kept, total] as [number, number]] : [])] };
46
86
  }
47
87
 
48
88
  /** Signals a shell reports as 128+n; the common ones a model can act on. */
@@ -69,17 +109,18 @@ export function commandHeader(input: {
69
109
  timeoutMs: number;
70
110
  /** What the model asked for, when that differs from what was applied. */
71
111
  requestedMs?: number;
112
+ managed?: boolean;
72
113
  }): string {
73
114
  const parts = [`exit ${input.exit}`, `${(input.wallMs / 1000).toFixed(1)}s`];
74
115
  if (input.stopped === "timeout") {
75
116
  const requested = input.requestedMs !== undefined && input.requestedMs !== input.timeoutMs
76
117
  ? ` (requested ${seconds(input.requestedMs)}, capped)` : "";
77
- // Say what to do differently: the same command with the same timeout
78
- // will time out the same way.
79
- parts.push(`timed out after ${seconds(input.timeoutMs)}${requested}; process group terminated; ` +
80
- "rerun with a larger timeout, or start long-lived work in the background with output redirected to a file and poll it");
118
+ // A deadline requests cleanup, but cannot by itself prove absence of
119
+ // partial side effects or successful cleanup (R3 / R10).
120
+ parts.push(`timed out after ${seconds(input.timeoutMs)}${requested}; process group cleanup requested; ` +
121
+ "inspect partial effects and cleanup before deciding on a new invocation or a larger timeout");
81
122
  } else if (input.stopped === "cancelled") {
82
- parts.push("cancelled; process group terminated");
123
+ parts.push("cancelled; process group cleanup requested");
83
124
  } else if (input.exit > 128 && SIGNALS[input.exit - 128] !== undefined) {
84
125
  parts.push(`128+${input.exit - 128}: likely terminated by ${SIGNALS[input.exit - 128]}`);
85
126
  }
@@ -36,7 +36,22 @@ export interface PermissionRule {
36
36
  */
37
37
  export type ToolSource = "builtin" | "extension";
38
38
 
39
+ /** Task capabilities are frozen once at session creation (O.7); not permissions. */
40
+ export const TOOL_PROFILES = {
41
+ coding: ["read", "grep", "find", "write", "edit", "environment", "exec_command", "write_stdin", "history", "note"],
42
+ readonly: ["read", "grep", "find", "history", "note"],
43
+ research: ["read", "grep", "find", "history", "note", "branch", "resolve"],
44
+ data: ["read", "grep", "find", "history", "note", "query"],
45
+ full: null,
46
+ } as const;
47
+ export type ToolProfile = keyof typeof TOOL_PROFILES;
48
+ export function isToolProfile(value: string): value is ToolProfile {
49
+ return Object.hasOwn(TOOL_PROFILES, value);
50
+ }
51
+
39
52
  export interface ToolSelection {
53
+ /** Applies only to builtin defaults; explicit only and extension capabilities win. */
54
+ readonly profile?: ToolProfile;
40
55
  /** `--tools`: a strict allowlist. */
41
56
  readonly only?: readonly string[];
42
57
  /** `--exclude-tools`: removed after the allowlist (or the full set) is built. */
@@ -69,6 +84,8 @@ export function defaultRules(): readonly PermissionRule[] {
69
84
  // Same reach as bash. write_stdin needs no rule of its own: it can only
70
85
  // address a session exec_command already started, under this approval.
71
86
  { action: "ask", tool: "exec_command" },
87
+ { action: "ask", tool: "environment" },
88
+ { action: "ask", tool: "target" },
72
89
  { action: "allow" },
73
90
  ];
74
91
  }
@@ -147,6 +164,11 @@ export function selectTools<T extends { name: string; source?: ToolSource }>(
147
164
  ): T[] {
148
165
  const names = new Set(selectToolNames(tools.map((t) => t.name), selection));
149
166
  let selected = tools.filter((t) => names.has(t.name));
167
+ const profile = selection.profile === undefined ? null : TOOL_PROFILES[selection.profile];
168
+ if (profile !== null && selection.only === undefined) {
169
+ const defaults = new Set<string>(profile);
170
+ selected = selected.filter(t => t.source === "extension" || defaults.has(t.name));
171
+ }
150
172
  // `--tools` already named specific tools, so the category filter does
151
173
  // not get a second veto. Untagged tools are builtin: that is today's
152
174
  // only producer, and treating absence as "keep" would make the flag a
@@ -158,10 +180,10 @@ export function selectTools<T extends { name: string; source?: ToolSource }>(
158
180
  }
159
181
 
160
182
  export function permissionRequest(name: string, input: unknown): PermissionRequest {
161
- if ((name !== "bash" && name !== "exec_command") || typeof input !== "object" || input === null) {
183
+ if (!["bash", "exec_command", "environment"].includes(name) || typeof input !== "object" || input === null) {
162
184
  return { tool: name };
163
185
  }
164
- const command = (input as { readonly [k: string]: unknown })[name === "bash" ? "command" : "cmd"];
186
+ const command = (input as { readonly [k: string]: unknown })[name === "exec_command" ? "cmd" : "command"];
165
187
  if (typeof command !== "string") return { tool: name };
166
188
  return { tool: name, command };
167
189
  }
@@ -0,0 +1,105 @@
1
+ /** R7 / D8 / D11: an observed process and a live control capability are distinct facts. */
2
+ import type { ArtifactRef, LogEvent } from "./events.ts";
3
+ import { isObservationArtifact } from "./observation-view.ts";
4
+ import { isCommandArtifactReceipt, type CommandArtifactReceipt } from "./command-artifacts.ts";
5
+
6
+ export type ProcessSource = { host: string; workspace: string; cwd: string; runtime: string };
7
+ export type ProcessReceipt = {
8
+ kind: "process";
9
+ version: 1;
10
+ session_id: number;
11
+ state: "running" | "exited" | "cancelled" | "timeout" | "unknown";
12
+ capability: "live" | "closed" | "lost" | "expired" | "unknown";
13
+ /** Legacy running/exit fields keep their meaning; null explicitly means no observation. */
14
+ running: boolean | null;
15
+ exit: number | null;
16
+ source: ProcessSource | null;
17
+ output?: ArtifactRef;
18
+ full_output?: ArtifactRef;
19
+ /** Half-open raw stream byte offsets covered by output, not preview character positions. */
20
+ cursor?: { unit: "bytes"; start: number; end: number };
21
+ replayed: boolean;
22
+ /** Complete means stream EOF was observed; full_output always means all captured bytes. */
23
+ capture?: "complete" | "partial";
24
+ /** Same one-shot command observation, not a new scan at each poll. */
25
+ artifacts?: CommandArtifactReceipt;
26
+ origin?: { node: number; event: string };
27
+ };
28
+
29
+ const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
30
+ const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
31
+ export function isProcessReceipt(value: unknown): value is ProcessReceipt {
32
+ if (!object(value) || value["kind"] !== "process" || value["version"] !== 1 || !natural(value["session_id"]) || value["session_id"] < 1 ||
33
+ !["running", "exited", "cancelled", "timeout", "unknown"].includes(String(value["state"])) ||
34
+ !["live", "closed", "lost", "expired", "unknown"].includes(String(value["capability"])) || typeof value["replayed"] !== "boolean") return false;
35
+ if (value["state"] === "running") {
36
+ if (value["running"] !== true || value["exit"] !== null || !["live", "lost"].includes(String(value["capability"]))) return false;
37
+ } else if (value["state"] === "unknown") {
38
+ if (value["running"] !== null || value["exit"] !== null || value["capability"] !== "unknown") return false;
39
+ } else if (value["running"] !== false || !Number.isInteger(value["exit"]) || !["closed", "expired"].includes(String(value["capability"]))) return false;
40
+ const source = value["source"];
41
+ if (["lost", "expired"].includes(String(value["capability"])) && value["replayed"] !== true) return false;
42
+ if (value["capture"] !== undefined && !["complete", "partial"].includes(String(value["capture"]))) return false;
43
+ const artifacts = value["artifacts"];
44
+ if (artifacts !== undefined && (!isCommandArtifactReceipt(artifacts) || artifacts.exit !== value["exit"] ||
45
+ value["state"] === "unknown" || !object(value["source"]) || !["host", "workspace", "cwd", "runtime"].every(key =>
46
+ (artifacts.source as Record<string, string>)[key] === (value["source"] as Record<string, unknown>)[key]))) return false;
47
+ if (source !== null && (!object(source) || !["host", "workspace", "cwd", "runtime"].every(key => typeof source[key] === "string" && source[key] !== ""))) return false;
48
+ for (const name of ["output", "full_output"]) if (value[name] !== undefined && !isObservationArtifact(value[name])) return false;
49
+ const cursor = value["cursor"];
50
+ if (cursor !== undefined && (!object(cursor) || cursor["unit"] !== "bytes" || !natural(cursor["start"]) || !natural(cursor["end"]) ||
51
+ cursor["end"] < cursor["start"] || !isObservationArtifact(value["output"]) || value["output"].bytes !== cursor["end"] - cursor["start"])) return false;
52
+ if (isObservationArtifact(value["full_output"]) && object(cursor) && typeof cursor["end"] === "number" && value["full_output"].bytes !== cursor["end"]) return false;
53
+ const origin = value["origin"];
54
+ return origin === undefined || object(origin) && natural(origin["node"]) && typeof origin["event"] === "string" && origin["event"] !== "";
55
+ }
56
+
57
+ export function processReceipt(input: { session: number; source: ProcessSource; exit: number | null; stopped?: "cancelled" | "timeout";
58
+ output: ArtifactRef; fullOutput?: ArtifactRef; start: number; end: number; capture?: "complete" | "partial"; replayed?: boolean; artifacts?: CommandArtifactReceipt }): ProcessReceipt {
59
+ const state = input.exit === null ? "running" : input.stopped ?? "exited";
60
+ return { kind: "process", version: 1, session_id: input.session, source: input.source, state,
61
+ capability: input.exit === null ? "live" : "closed", running: input.exit === null, exit: input.exit,
62
+ output: input.output, ...(input.fullOutput ? { full_output: input.fullOutput } : {}),
63
+ cursor: { unit: "bytes", start: input.start, end: input.end }, replayed: input.replayed === true,
64
+ ...(input.artifacts ? { artifacts: input.artifacts } : {}),
65
+ ...(input.capture ? { capture: input.capture } : {}) };
66
+ }
67
+
68
+ /** Missing live state never turns a historical running observation into a fabricated exit. */
69
+ export function recoverProcessReceipt(receipt: ProcessReceipt, origin: { node: number; event: string }): ProcessReceipt {
70
+ return { ...receipt, capability: receipt.state === "running" ? "lost" : receipt.state === "unknown" ? "unknown" : "expired", replayed: true, origin };
71
+ }
72
+
73
+ /** Original results, not a bounded handoff or replayed receipt, own the observation time and evidence. */
74
+ export function latestProcessReceipt(nodes: readonly { node: number; events: readonly LogEvent[] }[], session: number):
75
+ { receipt: ProcessReceipt; origin: { node: number; event: string } } | undefined {
76
+ for (let n = nodes.length - 1; n >= 0; n--) {
77
+ const part = nodes[n]!;
78
+ for (let i = part.events.length - 1; i >= 0; i--) {
79
+ const event = part.events[i]!, origin = { node: part.node, event: event.id };
80
+ if (event.t === "process_exit" && event.session === session) return { origin, receipt: event.receipt ?? {
81
+ kind: "process", version: 1, session_id: session, state: "exited", capability: "closed", source: null,
82
+ running: false, exit: event.exit, output: event.output, replayed: false,
83
+ } };
84
+ if (event.t !== "tool_result" || !object(event.data) || event.data["session_id"] !== session) continue;
85
+ if (isProcessReceipt(event.data)) {
86
+ if (!event.data.replayed) return { receipt: event.data, origin };
87
+ continue;
88
+ }
89
+ const data = event.data;
90
+ if (typeof data["running"] !== "boolean" || data["running"] === false && !Number.isInteger(data["exit"])) continue;
91
+ return { origin, receipt: { kind: "process", version: 1, session_id: session, source: null,
92
+ state: data["running"] ? "running" : "exited", capability: data["running"] ? "live" : "closed",
93
+ running: data["running"], exit: data["running"] ? null : data["exit"] as number,
94
+ ...(event.content_ref ? { output: event.content_ref } : {}), replayed: false } };
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ /** Disposable cache bounds are supplied by runtime; pruning never changes the durable receipts. */
101
+ export function retainProcessReceipts<T extends { endedAt: number }>(entries: readonly T[], now: number, maxAgeMs: number, maxCount: number): T[] {
102
+ if (!Number.isFinite(now) || !Number.isFinite(maxAgeMs) || maxAgeMs < 0 || !Number.isSafeInteger(maxCount) || maxCount < 0) throw new Error("invalid receipt retention policy");
103
+ return entries.filter(entry => entry.endedAt <= now && now - entry.endedAt < maxAgeMs)
104
+ .sort((a, b) => b.endedAt - a.endedAt).slice(0, maxCount);
105
+ }
@@ -0,0 +1,37 @@
1
+ /** O.14: observed process facts and present capabilities are different projections. */
2
+ import type { ArtifactRef, LogEvent } from "./events.ts";
3
+ import type { BranchRecord } from "./handoff.ts";
4
+ import { isProcessReceipt } from "./process-receipt.ts";
5
+
6
+ export type ProcessObservation = {
7
+ session: number; node: number; event: string; observed: "running" | "exited";
8
+ command?: string; exit?: number; output?: ArtifactRef;
9
+ }
10
+ export function deriveProcesses(prev: readonly ProcessObservation[], events: readonly LogEvent[], node: number): ProcessObservation[] {
11
+ const state = new Map(prev.map(item => [item.session, item]));
12
+ const commands = new Map<string, string>();
13
+ for (const event of events) {
14
+ if (event.t === "tool_invocation" && event.name === "exec_command" && typeof event.input === "object" && event.input !== null &&
15
+ !Array.isArray(event.input) && "cmd" in event.input && typeof event.input["cmd"] === "string") commands.set(event.call_id, event.input["cmd"]);
16
+ if (event.t === "assistant") for (const call of event.blocks) {
17
+ if (call.type === "tool_use" && call.name === "exec_command" && typeof call.input === "object" && call.input !== null && "cmd" in call.input && typeof call.input.cmd === "string") commands.set(call.call_id, call.input.cmd);
18
+ }
19
+ if (event.t === "process_exit") state.set(event.session, { session: event.session, node, event: event.id, observed: "exited", command: event.command, exit: event.exit, output: event.output });
20
+ if (event.t !== "tool_result" || !event.data || typeof event.data !== "object" || Array.isArray(event.data)) continue;
21
+ const data = event.data as { readonly [key: string]: unknown };
22
+ if (isProcessReceipt(data) && data.replayed) continue;
23
+ if (typeof data["session_id"] !== "number" || !Number.isSafeInteger(data["session_id"]) || data["session_id"] < 1 || typeof data["running"] !== "boolean") continue;
24
+ if (data["running"] === false && !Number.isInteger(data["exit"])) continue;
25
+ const before = state.get(data["session_id"]);
26
+ const command = event.effect?.kind === "command" ? event.effect.cmd : commands.get(event.call_id) ?? before?.command;
27
+ state.set(data["session_id"], { session: data["session_id"], node, event: event.id, observed: data["running"] ? "running" : "exited",
28
+ ...(command ? { command } : {}), ...(typeof data["exit"] === "number" ? { exit: data["exit"] } : {}), ...(event.content_ref ? { output: event.content_ref } : {}) });
29
+ }
30
+ return [...state.values()].sort((a, b) => a.session - b.session);
31
+ }
32
+ export function processStatus(item: ProcessObservation, handle: "running" | "ended_unobserved" | undefined): "running" | "ended_unobserved" | "unknown" | "exited" {
33
+ return item.observed === "exited" ? "exited" : handle ?? "unknown";
34
+ }
35
+ export function branchStatus(item: BranchRecord, handle: "running" | "ready" | undefined): "running" | "ready" | "unknown" | "joined" | "resolved" {
36
+ return item.resolution !== null ? "resolved" : item.handoff_ref ? "joined" : handle ?? "unknown";
37
+ }
@@ -0,0 +1,31 @@
1
+ /** OSC 7501 revision 0.2: program state, never a model's claim of goal completion (Q24/D11). */
2
+ import type { ExecutionOutcome } from "./outcome.ts";
3
+
4
+ export type ProgramStatus = { state: "idle" | "working" | "done" | "blocked" | "error" | "clear";
5
+ kind?: "permission" | "question" | "auth"; msg?: string };
6
+ export function programStatus(input: { running: boolean; waiting?: "permission" | "question" | "auth" | null;
7
+ outcome?: ExecutionOutcome; goal?: string }): ProgramStatus {
8
+ if (input.waiting) return { state: "blocked", kind: input.waiting, msg: input.waiting === "auth" ? "Sign in to continue" : input.waiting === "permission" ? "Tool approval required" : "User input required" };
9
+ if (input.running) return { state: "working", msg: "Agent is working" };
10
+ if (input.outcome?.status === "cancelled") return { state: "idle", msg: "Turn cancelled" };
11
+ if (input.outcome?.status === "failed") return { state: "error", msg: "Turn failed; inspect the session" };
12
+ if (input.outcome?.status === "stopped") return { state: "blocked", kind: "question", msg: "Turn stopped; review evidence before continuing" };
13
+ if (input.goal === "awaiting_human" || input.goal === "blocked") return { state: "blocked", kind: "question", msg: "Goal awaits user review" };
14
+ if (input.outcome?.reason === "model_end") return { state: "done", msg: input.goal === "verified" ? "Goal verified against its contract" : "Turn finished; goal verification incomplete" };
15
+ return { state: "idle", msg: "Ready for a request" };
16
+ }
17
+ export const PROGRAM_STATUS_QUERY = "\x1b]7501;?\x1b\\";
18
+ export function programStatusReply(sequence: string): boolean { return /^\x1b\]7501;\?[^\x00-\x1f\x7f]*(?:\x07|\x1b\\)$/.test(sequence); }
19
+
20
+ export function encodeProgramStatus(status: ProgramStatus, id: string): string {
21
+ if (id.length > 128 || id.split("/").length > 8 || !id.split("/").every(part => /^[A-Za-z0-9_.+-]{1,32}$/.test(part))) throw Error("invalid program status id");
22
+ let message = "", size = 0;
23
+ // Protocol's decoded UTF-8 cap. Remove controls before encoding; never split a code point.
24
+ for (const point of (status.msg ?? "").replace(/[\x00-\x1f\x7f-\x9f\p{Cf}]/gu, " ")) {
25
+ const bytes = new TextEncoder().encode(point); if (size + bytes.length > 2048) break;
26
+ message += point; size += bytes.length;
27
+ }
28
+ const encoded = btoa(Array.from(new TextEncoder().encode(message), byte => String.fromCharCode(byte)).join(""));
29
+ return `\x1b]7501;state=${status.state}:app=xeno:id=${id}` +
30
+ (status.state === "blocked" && status.kind ? `:kind=${status.kind}` : "") + (message ? `:msg=${encoded}` : "") + "\x1b\\";
31
+ }