@visual-z/xeno 0.8.3 → 0.9.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 (348) hide show
  1. package/CHANGELOG.md +24 -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 -2066
  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 -1713
  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 -3001
  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/canvas.ts +0 -96
  312. package/src/tui/ui/colors.ts +0 -70
  313. package/src/tui/ui/composer.ts +0 -200
  314. package/src/tui/ui/control-panel.ts +0 -25
  315. package/src/tui/ui/control-sidebar.ts +0 -49
  316. package/src/tui/ui/dialog.ts +0 -398
  317. package/src/tui/ui/draft.ts +0 -23
  318. package/src/tui/ui/duration.ts +0 -16
  319. package/src/tui/ui/edge.ts +0 -186
  320. package/src/tui/ui/image-size.ts +0 -44
  321. package/src/tui/ui/ink.ts +0 -210
  322. package/src/tui/ui/items.ts +0 -42
  323. package/src/tui/ui/keys.ts +0 -40
  324. package/src/tui/ui/layout.ts +0 -74
  325. package/src/tui/ui/list-model.ts +0 -86
  326. package/src/tui/ui/list-view.ts +0 -172
  327. package/src/tui/ui/mascot.ts +0 -67
  328. package/src/tui/ui/messages.ts +0 -518
  329. package/src/tui/ui/motion.ts +0 -28
  330. package/src/tui/ui/paths.ts +0 -10
  331. package/src/tui/ui/pictures.ts +0 -108
  332. package/src/tui/ui/screen.ts +0 -548
  333. package/src/tui/ui/status.ts +0 -403
  334. package/src/tui/ui/styled.ts +0 -85
  335. package/src/tui/ui/tool-card.ts +0 -150
  336. package/src/tui/width.ts +0 -567
  337. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  338. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  339. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  340. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  341. package/src/vendor/pi-rich/html.ts +0 -51
  342. package/src/vendor/pi-rich/image-convert.ts +0 -49
  343. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  344. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  345. package/src/vendor/pi-rich/image-resize.ts +0 -123
  346. package/src/vendor/pi-rich/mime.ts +0 -116
  347. package/src/vendor/pi-rich/photon.ts +0 -139
  348. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,94 +0,0 @@
1
- /** R8: declarations select a target; only an owned runtime observation supplies its identity. */
2
- import { parseCommandArtifactPlan, validArtifactPath, type CommandArtifactPlan } from "./command-artifacts.ts";
3
- import type { ArtifactRef } from "./events.ts";
4
- import { isObservationArtifact } from "./observation-view.ts";
5
- import type { ProcessSource } from "./process-receipt.ts";
6
-
7
- export type RuntimeTarget = { id: string; build: string; artifacts: CommandArtifactPlan; entry: string;
8
- interpreter?: string; args?: string[]; workdir?: string; port?: number };
9
- export type TargetBuild = { node: number; event: string; manifest: ArtifactRef };
10
- export type RuntimeInstance = { id: string; session: number; pid: number; source: ProcessSource;
11
- entry: { path: string; sha256: string }; program: { path: string; sha256: string; device: string; inode: string } };
12
- export type RuntimeTargetReceipt = {
13
- kind: "runtime_target"; version: 1; target: string; request: string; contract_event: string;
14
- state: "ready" | "not_ready" | "stale" | "unknown"; reason: string; observed_at: string;
15
- source: ProcessSource; build?: TargetBuild; instance?: RuntimeInstance; port?: number;
16
- checks: { build: boolean; inputs: boolean; artifacts: boolean; process: boolean; image: boolean; port: boolean | null };
17
- manifest?: ArtifactRef;
18
- };
19
- export type TargetAcceptance = { id: string; build: string; status: "not_run" | RuntimeTargetReceipt["state"];
20
- observation?: string; binding?: string; session?: number; pid?: number; entry?: string; port?: number; build_event?: string; reason?: string };
21
-
22
- const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
23
- const id = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(value);
24
- const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
25
- const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
26
- const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
27
- const source = (value: unknown): value is ProcessSource => record(value) && ["host", "workspace", "cwd", "runtime"].every(key => text(value[key]));
28
-
29
- export function parseRuntimeTarget(value: unknown): RuntimeTarget {
30
- if (!record(value) || Object.keys(value).some(key => !["id", "build", "artifacts", "entry", "interpreter", "args", "workdir", "port"].includes(key)) ||
31
- !id(value["id"]) || !id(value["build"]) || !validArtifactPath(value["entry"])) throw Error("invalid runtime target declaration");
32
- const artifacts = parseCommandArtifactPlan(value["artifacts"]);
33
- if (!artifacts.paths.includes(value["entry"])) throw Error("target entry must be an observed build artifact");
34
- const interpreter = value["interpreter"], args = value["args"], workdir = value["workdir"], port = value["port"];
35
- if (interpreter !== undefined && (typeof interpreter !== "string" || !interpreter.startsWith("/") || /[\x00-\x1f\x7f]/.test(interpreter) || interpreter.length > 4096)) throw Error("target interpreter needs an explicit absolute path");
36
- // Assumed argv budget prevents declarations from becoming an unbounded prompt or exec payload.
37
- if (args !== undefined && (!Array.isArray(args) || args.length > 64 || !args.every(arg => typeof arg === "string" && !arg.includes("\0")) || JSON.stringify(args).length > 8192)) throw Error("target args must be a bounded literal string array");
38
- if (workdir !== undefined && workdir !== "." && !validArtifactPath(workdir)) throw Error("target workdir must stay inside the workspace");
39
- if (port !== undefined && (!natural(port) || port < 1 || port > 65535)) throw Error("target port must be 1..65535 on IPv4 loopback");
40
- return { id: value["id"], build: value["build"], entry: value["entry"], artifacts,
41
- ...(interpreter !== undefined ? { interpreter: interpreter as string } : {}), ...(args !== undefined ? { args: [...args as string[]] } : {}),
42
- ...(workdir !== undefined ? { workdir: workdir as string } : {}), ...(port !== undefined ? { port: port as number } : {}) };
43
- }
44
-
45
- export function targetIsReady(receipt: Pick<RuntimeTargetReceipt, "build" | "instance" | "checks">): boolean {
46
- return receipt.build !== undefined && receipt.instance !== undefined && receipt.checks.build && receipt.checks.inputs && receipt.checks.artifacts &&
47
- receipt.checks.process && receipt.checks.image && receipt.checks.port !== false;
48
- }
49
- export function isRuntimeTargetReceipt(value: unknown): value is RuntimeTargetReceipt {
50
- if (!record(value) || value["kind"] !== "runtime_target" || value["version"] !== 1 || !id(value["target"]) || !text(value["request"]) ||
51
- !text(value["contract_event"]) || !text(value["observed_at"]) || !text(value["reason"]) || !source(value["source"]) ||
52
- !["ready", "not_ready", "stale", "unknown"].includes(String(value["state"]))) return false;
53
- const build = value["build"], instance = value["instance"], checks = value["checks"], port = value["port"];
54
- if (build !== undefined && (!record(build) || !natural(build["node"]) || !text(build["event"]) || !isObservationArtifact(build["manifest"]))) return false;
55
- if (instance !== undefined && (!record(instance) || !text(instance["id"]) || !natural(instance["session"]) || instance["session"] < 1 ||
56
- !natural(instance["pid"]) || instance["pid"] < 1 || !source(instance["source"]) ||
57
- !["host", "workspace", "cwd", "runtime"].every(key => (instance["source"] as Record<string, string>)[key] === (value["source"] as Record<string, string>)[key]) ||
58
- !record(instance["entry"]) || !text(instance["entry"]["path"]) || !digest(instance["entry"]["sha256"]) ||
59
- !record(instance["program"]) || !text(instance["program"]["path"]) || !digest(instance["program"]["sha256"]) ||
60
- !text(instance["program"]["device"]) || !text(instance["program"]["inode"]))) return false;
61
- if (!record(checks) || !["build", "inputs", "artifacts", "process", "image"].every(key => typeof checks[key] === "boolean") ||
62
- !(checks["port"] === null || typeof checks["port"] === "boolean") ||
63
- (port === undefined ? checks["port"] !== null : !natural(port) || port < 1 || port > 65535 || checks["port"] === null)) return false;
64
- if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
65
- return value["state"] !== "ready" || targetIsReady(value as RuntimeTargetReceipt) && isObservationArtifact(value["manifest"]);
66
- }
67
-
68
- /** lsof -F0 is a field protocol; matching a process name or display substring proves nothing. */
69
- export function lsofFiles(output: string, pid: number): Record<string, string>[] {
70
- let process = -1, file: Record<string, string> | undefined;
71
- const files: Record<string, string>[] = [];
72
- for (const chunk of output.split("\0")) {
73
- const field = chunk.replace(/^\n/, "");
74
- if (!field) continue;
75
- const key = field[0]!, value = field.slice(1);
76
- if (key === "p") { process = Number(value); file = undefined; }
77
- else if (key === "f") { file = { f: value }; if (process === pid) files.push(file); }
78
- else if (file && process === pid) {
79
- if (key === "T") { const split = value.indexOf("="); if (split > 0) file[`T${value.slice(0, split)}`] = value.slice(split + 1); }
80
- else file[key] = value;
81
- }
82
- }
83
- return files;
84
- }
85
- export function lsofOwnsImage(output: string, pid: number, device: string, inode: string): boolean {
86
- return lsofFiles(output, pid).some(file => {
87
- if (file["f"] !== "txt" || file["i"] !== inode || !/^0x[0-9a-f]+$/i.test(file["D"] ?? "")) return false;
88
- try { return BigInt(file["D"]!) === BigInt(device); } catch { return false; }
89
- });
90
- }
91
- export function lsofOwnsPort(output: string, pid: number, port: number): boolean {
92
- return lsofFiles(output, pid).some(file => file["t"] === "IPv4" && file["P"] === "TCP" && file["TST"] === "LISTEN" &&
93
- ["*", "127.0.0.1"].some(host => file["n"] === `${host}:${port}`));
94
- }
@@ -1,8 +0,0 @@
1
- /** O.5: shared work may overlap only before the next exclusive barrier. */
2
- export function schedulingWindow(modes: readonly ("parallel" | "exclusive")[], start: number, limit: number): number {
3
- if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("maxParallelTools must be a positive integer");
4
- if (modes[start] !== "parallel") return Math.min(start + 1, modes.length);
5
- let end = start;
6
- while (end < modes.length && modes[end] === "parallel" && end - start < limit) end++;
7
- return end;
8
- }
@@ -1,97 +0,0 @@
1
- /** R2: scan completeness and preview completeness answer different questions. */
2
- import { headTailSelection, isObservationArtifact, isBytePreview, type BytePreview } from "./observation-view.ts";
3
- import type { ArtifactRef } from "./events.ts";
4
- import { artifactRecovery } from "./prompt-policy.ts";
5
-
6
- export interface SearchRow { prefix: string; text: string; }
7
- export interface SearchScan {
8
- status: "complete" | "partial" | "unknown";
9
- reasons: { code: string; path?: string }[];
10
- examined_files: number | null;
11
- source_versions: "recorded" | "unknown";
12
- }
13
- export interface SearchObservation {
14
- engine: "walk" | "ripgrep" | "fd" | "directory";
15
- rows: SearchRow[];
16
- scan: SearchScan;
17
- sources: { path: string; sha256: string }[];
18
- raw?: Uint8Array;
19
- rawErrors?: Uint8Array;
20
- limit: number;
21
- empty: string;
22
- }
23
-
24
- export interface SearchReceipt {
25
- kind: "search";
26
- version: 1;
27
- source: { host: string; workspace: string; resource: string };
28
- engine: SearchObservation["engine"];
29
- scan: { status: SearchScan["status"]; reasons: string[]; examined_files: number | null; source_versions: SearchScan["source_versions"] };
30
- matches: number;
31
- artifact: ArtifactRef;
32
- manifest: ArtifactRef;
33
- raw?: ArtifactRef;
34
- raw_errors?: ArtifactRef;
35
- preview: BytePreview;
36
- }
37
-
38
- const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
39
- const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
40
- export function isSearchReceipt(value: unknown): value is SearchReceipt {
41
- if (!object(value) || value["kind"] !== "search" || value["version"] !== 1 || !isObservationArtifact(value["artifact"]) || !isObservationArtifact(value["manifest"]) || !natural(value["matches"])) return false;
42
- if (!["walk", "ripgrep", "fd", "directory"].includes(String(value["engine"])) || value["raw"] !== undefined && !isObservationArtifact(value["raw"])) return false;
43
- if (value["raw_errors"] !== undefined && !isObservationArtifact(value["raw_errors"])) return false;
44
- const source = value["source"], scan = value["scan"];
45
- if (!object(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "")) return false;
46
- if (!object(scan) || !["complete", "partial", "unknown"].includes(String(scan["status"])) || !["recorded", "unknown"].includes(String(scan["source_versions"])) ||
47
- !Array.isArray(scan["reasons"]) || !scan["reasons"].every(reason => typeof reason === "string") || scan["examined_files"] !== null && !natural(scan["examined_files"])) return false;
48
- if (scan["status"] === "complete" && scan["reasons"].length !== 0 || scan["status"] !== "complete" && scan["reasons"].length === 0) return false;
49
- return isBytePreview(value["preview"], value["artifact"].bytes);
50
- }
51
-
52
- const encoder = new TextEncoder();
53
- const bytes = (text: string) => encoder.encode(text).length;
54
- export const searchObservationText = (rows: readonly SearchRow[]): string => rows.map(row => row.prefix + row.text).join("\n");
55
-
56
- /** Map count, per-line, and UTF-8 byte cuts back to the same immutable full observation. */
57
- export function previewSearchObservation(observation: SearchObservation, artifact: string, maxBytes: number, maxLineChars = 500, notice = "", tools?: readonly string[]) {
58
- let text = "", source = 0, view = 0;
59
- const spans: { view: number; source: number; bytes: number }[] = [];
60
- const mapped = (value: string) => { const size = bytes(value); if (size) spans.push({ view, source, bytes: size }); text += value; view += size; };
61
- const synthetic = (value: string) => { text += value; view += bytes(value); };
62
- if (notice) synthetic(notice + "\n");
63
- const shown = Math.max(0, Math.min(observation.rows.length, observation.limit));
64
- for (let index = 0; index < observation.rows.length; index++) {
65
- const row = observation.rows[index]!;
66
- if (index < shown) {
67
- let end = Math.min(maxLineChars, row.text.length);
68
- if (end > 0 && end < row.text.length && /[\uD800-\uDBFF]/.test(row.text[end - 1]!)) end--;
69
- const kept = row.prefix + row.text.slice(0, end);
70
- mapped(kept);
71
- if (end < row.text.length) synthetic(`… [line truncated; ${artifactRecovery(artifact, source + bytes(kept), tools)}]`);
72
- }
73
- source += bytes(row.prefix + row.text);
74
- if (index < observation.rows.length - 1) { if (index < shown) mapped("\n"); source++; }
75
- }
76
- if (!observation.rows.length) synthetic(observation.scan.status === "complete" ? observation.empty : "[No matches observed in the scanned portion]");
77
- if (shown < observation.rows.length) synthetic(`[${observation.rows.length - shown} more matches omitted; ${artifactRecovery(artifact, 0, tools)}]`);
78
- if (observation.scan.status !== "complete") synthetic(`\n[scan ${observation.scan.status}: ${[...new Set(observation.scan.reasons.map(reason => reason.code === "match_timeout" ? "search cut off: match_timeout" : reason.code))].join(", ")}]`);
79
- const previewBytes = encoder.encode(text);
80
- const selected = headTailSelection(previewBytes, previewBytes, previewBytes.length, maxBytes,
81
- omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
82
- const visible: [number, number][] = [];
83
- for (const [lo, hi] of selected.visible) for (const span of spans) {
84
- const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
85
- if (end <= begin) continue;
86
- const a = span.source + begin - span.view, b = span.source + end - span.view;
87
- const last = visible.at(-1);
88
- if (last?.[1] === a) last[1] = b;
89
- else visible.push([a, b]);
90
- }
91
- let next = 0;
92
- for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
93
- const full = encoder.encode(searchObservationText(observation.rows));
94
- const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(lo, hi))).join("");
95
- return { text: selected.text, semantic, preview: { unit: "utf8_bytes" as const, complete: next === source,
96
- visible, next_offset: next === source ? null : next } };
97
- }
@@ -1,188 +0,0 @@
1
- /**
2
- * Credential redaction at the log boundary (S.9).
3
- *
4
- * The log is the only truth and every model request is a projection of it, so
5
- * a credential that reaches the log is resent on every later request and
6
- * survives every cut through `objective` and `user_turns` (D2). Masking at
7
- * projection time would leave the original in an append-only file forever.
8
- * Redaction therefore happens before an event is written: the secret is
9
- * replaced by a placeholder, and the value lives in a separate per-session
10
- * vault that no projection reads.
11
- *
12
- * The placeholder is an exact inverse: tools expand it back to the value when
13
- * they run (OpenHands' SecretRegistry does the same with secret names), and
14
- * any output that echoes a known value is masked again. A false positive
15
- * therefore costs readability, never correctness -- an edit whose old_string
16
- * holds a placeholder still matches the file.
17
- *
18
- * Only high-confidence shapes are detected (gitleaks prefix rules plus
19
- * "secret-named key = value"); entropy detection is deliberately absent because
20
- * its false positives on hashes and code are irreversible in a live view.
21
- * A bare password has no shape, so it is recognised only by context: a short
22
- * token the user sends right after the assistant asked for a credential.
23
- */
24
-
25
- export interface SecretEntry {
26
- /** The placeholder's number: `[[xeno-secret-<n>]]`. */
27
- n: number;
28
- value: string;
29
- /** Which rule found it, for the audit trail; never the value. */
30
- rule: string;
31
- }
32
-
33
- export type Vault = readonly SecretEntry[];
34
-
35
- export const placeholder = (n: number): string => `[[xeno-secret-${n}]]`;
36
- const PLACEHOLDER = /\[\[xeno-secret-(\d+)\]\]/g;
37
-
38
- /**
39
- * A known value shorter than this is too likely to occur by accident ("admin",
40
- * "123456") to mask everywhere it appears. Assumed, not measured; Codex's
41
- * key-assignment rule uses the same 8.
42
- */
43
- export const MIN_SECRET_LENGTH = 8;
44
-
45
- interface Rule { id: string; pattern: RegExp; /** Capture group holding the secret; 0 is the whole match. */ group: number }
46
-
47
- // Prefix rules follow gitleaks' default config (b58d3f10) and Gemini CLI's
48
- // environmentSanitization.ts; tskey- is added for Tailscale, which gitleaks lacks.
49
- const RULES: readonly Rule[] = [
50
- { id: "pem-private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g, group: 0 },
51
- { id: "anthropic-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/g, group: 0 },
52
- { id: "openai-key", pattern: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}/g, group: 0 },
53
- { id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})/g, group: 0 },
54
- { id: "gitlab-token", pattern: /\bglpat-[A-Za-z0-9_-]{20,}/g, group: 0 },
55
- { id: "slack-token", pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}/g, group: 0 },
56
- { id: "aws-access-key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, group: 0 },
57
- { id: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}/g, group: 0 },
58
- { id: "stripe-key", pattern: /\b[sr]k_live_[0-9A-Za-z]{24,}/g, group: 0 },
59
- // tskey-<kind>-<id with a digit>-<secret>; documentation like "tskey-auth-reusable-" has no digit in the id.
60
- { id: "tailscale-key", pattern: /\btskey-[a-z]+-[A-Za-z0-9]*\d[A-Za-z0-9]*-[A-Za-z0-9]{10,}/g, group: 0 },
61
- { id: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, group: 0 },
62
- { id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, group: 0 },
63
- { id: "bearer-token", pattern: /\bBearer\s+([A-Za-z0-9._~+/=-]{20,})/g, group: 1 },
64
- { id: "url-password", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:([^\s/@]{3,})@/gi, group: 1 },
65
- // A secret-named key assigned a literal. The value class excludes quotes,
66
- // brackets and parentheses so `token = getToken()` and `password: string`
67
- // stay code; requiring a digit or 16+ characters keeps `secret: undefined`.
68
- { id: "secret-assignment", group: 1,
69
- // Up to three prefix words, so access_token, HUILIANYI_REFRESH_TOKEN and db-password count.
70
- pattern: /\b(?:[a-z0-9]+[_-]){0,3}(?:api[_-]?key|access[_-]?key|secret(?:[_-]?key)?|client[_-]?secret|token|password|passwd|pwd)["']?\s*[:=]\s*["']?([^\s"'`(){}[\]<>;,]{8,})/gi },
71
- { id: "secret-assignment-zh", group: 1, pattern: /(?:密码|口令|密钥|令牌)\s*(?:是|为|[::=])\s*["'“]?([^\s"'”,。,;;]{4,})/g },
72
- ];
73
-
74
- const plausibleAssigned = (value: string): boolean =>
75
- !/^(?:\$|process\.|os\.|env\.|self\.|this\.)/.test(value) && !value.startsWith("[[xeno-secret-") &&
76
- // A pure number after a token-named key is a count (input_tokens=12000000), not a credential.
77
- !/^\d+$/.test(value) &&
78
- // Credentials carry digits; a digit-free value is a path, a member access or a placeholder word
79
- // (replayed 2026-10-12 sessions: "internal/server/…", "auth.AccessToken"). A long mixed-case
80
- // value without separators is the one digit-free exception.
81
- (/\d/.test(value) && /[A-Za-z]/.test(value) || value.length >= 20 && /[A-Z]/.test(value) && /[a-z]/.test(value) && !/[./]/.test(value));
82
-
83
- /** Every secret-shaped span in `text`, with the rule that found it. Overlaps resolve to the earliest, longest. */
84
- export function findSecrets(text: string): { start: number; end: number; rule: string }[] {
85
- const spans: { start: number; end: number; rule: string }[] = [];
86
- for (const rule of RULES) {
87
- for (const match of text.matchAll(rule.pattern)) {
88
- const value = match[rule.group];
89
- if (value === undefined || match.index === undefined) continue;
90
- if (rule.id === "secret-assignment" && !plausibleAssigned(value)) continue;
91
- const start = match.index + match[0].lastIndexOf(value);
92
- spans.push({ start, end: start + value.length, rule: rule.id });
93
- }
94
- }
95
- spans.sort((a, b) => a.start - b.start || b.end - a.end);
96
- const kept: typeof spans = [];
97
- for (const span of spans) if (kept.length === 0 || span.start >= kept.at(-1)!.end) kept.push(span);
98
- return kept;
99
- }
100
-
101
- /**
102
- * Replaces known values and newly found secrets with placeholders. Returns the
103
- * vault entries it had to add; the caller persists them before the text is
104
- * written anywhere, so a placeholder in the log always has a value to expand to.
105
- */
106
- export function redactText(text: string, vault: Vault, candidates: readonly { value: string; rule: string }[] = []): { text: string; added: SecretEntry[] } {
107
- if (text.length === 0) return { text, added: [] };
108
- const added: SecretEntry[] = [];
109
- let next = vault.reduce((max, entry) => Math.max(max, entry.n), 0) + 1;
110
- // Candidates (secret-named environment values) join the vault only when they
111
- // actually appear, so a placeholder number is never spent on a value no event holds.
112
- const known: SecretEntry[] = [...vault];
113
- for (const candidate of candidates) {
114
- if (candidate.value.length < MIN_SECRET_LENGTH || !text.includes(candidate.value) || known.some(entry => entry.value === candidate.value)) continue;
115
- const entry = { n: next++, value: candidate.value, rule: candidate.rule };
116
- known.push(entry); added.push(entry);
117
- }
118
- let out = maskKnown(text, known);
119
- const spans = findSecrets(out);
120
- if (spans.length === 0) return { text: out, added };
121
- let result = "", at = 0;
122
- for (const span of spans) {
123
- const value = out.slice(span.start, span.end);
124
- const existing = known.find(entry => entry.value === value);
125
- const entry = existing ?? { n: next++, value, rule: span.rule };
126
- if (!existing) { known.push(entry); added.push(entry); }
127
- result += out.slice(at, span.start) + placeholder(entry.n);
128
- at = span.end;
129
- }
130
- // Values found in this pass may occur again where a neighbouring match split
131
- // them (two tokens with no delimiter); a second known-value pass closes that.
132
- return { text: maskKnown(result + out.slice(at), known), added };
133
- }
134
-
135
- /** Longest first, so a value containing another is masked whole. A short value is masked only where a rule found it. */
136
- function maskKnown(text: string, known: readonly SecretEntry[]): string {
137
- let out = text;
138
- for (const entry of known.filter(entry => entry.value.length >= MIN_SECRET_LENGTH).sort((a, b) => b.value.length - a.value.length)) {
139
- if (out.includes(entry.value)) out = out.split(entry.value).join(placeholder(entry.n));
140
- }
141
- return out;
142
- }
143
-
144
- /** The inverse of redactText for tool inputs. An unknown placeholder is left as written. */
145
- export function expandText(text: string, vault: Vault): string {
146
- if (!text.includes("[[xeno-secret-")) return text;
147
- return text.replace(PLACEHOLDER, (whole, n: string) => vault.find(entry => entry.n === Number(n))?.value ?? whole);
148
- }
149
-
150
- const ASKED_FOR_CREDENTIAL = /password|passcode|passphrase|credential|api[ _-]?key|access[ _-]?token|\btoken\b|密码|口令|密钥|凭据|凭证|令牌|验证码/i;
151
-
152
- /**
153
- * A bare password has no shape; what identifies it is the exchange. A message
154
- * that is a single token with mixed character classes, sent right after the
155
- * assistant asked for a credential, is that credential (the 2026-10-12 ticket
156
- * session: the assistant asked for a login password, the user replied with it).
157
- */
158
- export function isSolicitedSecret(userText: string, previousAssistantText: string | undefined): boolean {
159
- const value = userText.trim();
160
- if (previousAssistantText === undefined || !ASKED_FOR_CREDENTIAL.test(previousAssistantText)) return false;
161
- if (!/^\S{6,128}$/.test(value) || value.startsWith("[[xeno-secret-")) return false;
162
- // Letters with digits, or three character classes: "dsm612566" and "Pa$$word"
163
- // qualify, an identifier like "OPENAI_API_KEY" or a branch name does not.
164
- const classes = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter(pattern => pattern.test(value)).length;
165
- const lettersAndDigits = /[A-Za-z]/.test(value) && /\d/.test(value);
166
- return (lettersAndDigits || classes >= 3) && !/^[a-z]+:\/\//i.test(value) && !/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(value);
167
- }
168
-
169
- /** Applies `redact` to every string in a JSON value, preserving its shape. */
170
- export function mapStrings(value: unknown, redact: (text: string) => string, skip: (key: string) => boolean = () => false): unknown {
171
- if (typeof value === "string") return redact(value);
172
- if (Array.isArray(value)) return value.map(item => mapStrings(item, redact, skip));
173
- if (value !== null && typeof value === "object") {
174
- const out: Record<string, unknown> = {};
175
- for (const [key, item] of Object.entries(value)) out[key] = skip(key) ? item : mapStrings(item, redact, skip);
176
- return out;
177
- }
178
- return value;
179
- }
180
-
181
- /** Environment variables whose names say they hold a secret; their values are masked wherever they appear. */
182
- export function secretEnvironmentValues(env: Readonly<Record<string, string | undefined>>): { value: string; rule: string }[] {
183
- return Object.entries(env).flatMap(([name, value]) =>
184
- value !== undefined && value.length >= MIN_SECRET_LENGTH && !value.startsWith("/") &&
185
- /(?:^|_)(?:API_?KEY|KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIALS?|AUTH)(?:_|$)/i.test(name) &&
186
- !/(?:^|_)(?:PATH|DIR|FILE|URL|HOST|PORT|USER|NAME|ID|MODE|TYPE)$/i.test(name)
187
- ? [{ value, rule: `env:${name}` }] : []).sort((a, b) => a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0);
188
- }
@@ -1,4 +0,0 @@
1
- /** Archive paths are a closed storage vocabulary, never arbitrary filesystem destinations (D3). */
2
- export function archivePath(path: unknown): path is string {
3
- return typeof path === "string" && /^(?:node-\d{4,}\.jsonl|handoff-\d{4,}\.yaml|manifest\.json|metadata\.jsonl|(?:artifacts|blobs)\/[a-f0-9]{2}\/[a-f0-9]{62})$/.test(path);
4
- }
@@ -1,82 +0,0 @@
1
- /** Pi session-selector-search (MIT, 02eed88), independent of its AgentSession and clock. */
2
- export type SessionSort = "threaded" | "recent" | "relevance";
3
- export interface SessionRow {
4
- path: string; id: string; name: string | null; updated_at: string | null;
5
- workspace?: string; parent_session?: string; text?: string;
6
- }
7
- export interface SearchQuery {
8
- tokens: { kind: "fuzzy" | "phrase"; value: string }[];
9
- regex?: RegExp;
10
- error?: string;
11
- }
12
- export function parseSessionQuery(query: string): SearchQuery {
13
- const trimmed = query.trim();
14
- if (trimmed.startsWith("re:")) {
15
- if (!trimmed.slice(3).trim()) return { tokens: [], error: "Empty regex" };
16
- try { return { tokens: [], regex: new RegExp(trimmed.slice(3).trim(), "i") }; }
17
- catch (error) { return { tokens: [], error: String(error) }; }
18
- }
19
- const tokens: SearchQuery["tokens"] = [];
20
- let buffer = "", quoted = false;
21
- const flush = () => { if (buffer.trim()) tokens.push({ kind: quoted ? "phrase" : "fuzzy", value: buffer.trim() }); buffer = ""; };
22
- for (const char of trimmed) {
23
- if (char === '"') { flush(); quoted = !quoted; }
24
- else if (!quoted && /\s/.test(char)) flush();
25
- else buffer += char;
26
- }
27
- if (quoted) return { tokens: trimmed.split(/\s+/).filter(Boolean).map(value => ({ kind: "fuzzy", value })) };
28
- flush(); return { tokens };
29
- }
30
- export function selectSessions<T extends SessionRow>(rows: readonly T[], query: string, sort: SessionSort, named: boolean,
31
- fuzzy: (query: string, text: string) => { matches: boolean; score: number }): (T & { depth: number })[] {
32
- const parsed = parseSessionQuery(query);
33
- if (parsed.error) return [];
34
- const scored = rows.flatMap(row => {
35
- if (named && !row.name?.trim()) return [];
36
- const text = `${row.id} ${row.name ?? ""} ${row.text ?? ""} ${row.workspace ?? ""}`;
37
- let score = 0;
38
- if (parsed.regex) { const i = text.search(parsed.regex); if (i < 0) return []; score = i * 0.1; }
39
- else for (const token of parsed.tokens) {
40
- if (token.kind === "phrase") {
41
- const i = text.toLowerCase().replace(/\s+/g, " ").indexOf(token.value.toLowerCase().replace(/\s+/g, " "));
42
- if (i < 0) return []; score += i * 0.1;
43
- } else { const match = fuzzy(token.value, text); if (!match.matches) return []; score += match.score; }
44
- }
45
- return [{ row, score }];
46
- });
47
- const recent = (a: SessionRow, b: SessionRow) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "") || a.path.localeCompare(b.path);
48
- scored.sort((a, b) => query.trim() && sort !== "recent" ? a.score - b.score || recent(a.row, b.row) : recent(a.row, b.row));
49
- if (sort !== "threaded" || query.trim()) return scored.map(({ row }) => ({ ...row, depth: 0 }));
50
- const byPath = new Map(scored.map(({ row }) => [row.path, row]));
51
- const children = new Map<string, T[]>();
52
- const roots: T[] = [];
53
- for (const { row } of scored) {
54
- if (row.parent_session && row.parent_session !== row.path && byPath.has(row.parent_session)) {
55
- const list = children.get(row.parent_session) ?? []; list.push(row); children.set(row.parent_session, list);
56
- } else roots.push(row);
57
- }
58
- // Allocation can be arbitrarily deep. Iterative traversal handles cycles without hiding their rows.
59
- const latest = new Map(scored.map(({ row }) => [row.path, row.updated_at ?? ""]));
60
- for (const { row } of scored) {
61
- const visited = new Set<string>([row.path]);
62
- let parent = row.parent_session;
63
- while (parent && byPath.has(parent) && !visited.has(parent)) {
64
- visited.add(parent);
65
- if ((row.updated_at ?? "") > (latest.get(parent) ?? "")) latest.set(parent, row.updated_at ?? "");
66
- parent = byPath.get(parent)?.parent_session;
67
- }
68
- }
69
- const byActivity = (a: T, b: T) => (latest.get(b.path) ?? "").localeCompare(latest.get(a.path) ?? "") || recent(a, b);
70
- roots.sort(byActivity); for (const list of children.values()) list.sort(byActivity);
71
- const out: (T & { depth: number })[] = [], visited = new Set<string>();
72
- for (const root of [...roots, ...scored.map(s => s.row)]) {
73
- const stack = [{ row: root, depth: 0 }];
74
- while (stack.length) {
75
- const next = stack.pop()!;
76
- if (visited.has(next.row.path)) continue;
77
- visited.add(next.row.path); out.push({ ...next.row, depth: next.depth });
78
- for (const row of [...children.get(next.row.path) ?? []].reverse()) stack.push({ row, depth: next.depth + 1 });
79
- }
80
- }
81
- return out;
82
- }
@@ -1,61 +0,0 @@
1
- /** Session intent is decided before any log is opened (D8, Pi entry parity). */
2
- export interface SessionEntryFlags {
3
- explicit?: string;
4
- fresh?: boolean;
5
- continueLatest?: boolean;
6
- noSession?: boolean;
7
- session?: string;
8
- sessionId?: string;
9
- resume?: string | boolean;
10
- forkFrom?: string;
11
- }
12
-
13
- export type SessionEntry =
14
- | { kind: "new" }
15
- | { kind: "ephemeral" }
16
- | { kind: "continue" }
17
- | { kind: "select" }
18
- | { kind: "named"; id: string }
19
- | { kind: "open"; target: string; explicit: boolean }
20
- | { kind: "fork"; target: string };
21
-
22
- export function planSessionEntry(flags: SessionEntryFlags): SessionEntry {
23
- const choices: { flag: string; entry: SessionEntry }[] = [];
24
- if (flags.explicit) choices.push({ flag: "--log", entry: { kind: "open", target: flags.explicit, explicit: true } });
25
- if (flags.session) choices.push({ flag: "--session", entry: { kind: "open", target: flags.session, explicit: false } });
26
- if (flags.sessionId) {
27
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(flags.sessionId)) throw new Error("--session-id requires a plain session identifier");
28
- choices.push({ flag: "--session-id", entry: { kind: "named", id: flags.sessionId } });
29
- }
30
- if (flags.resume) choices.push({ flag: "--resume", entry: typeof flags.resume === "string"
31
- ? { kind: "open", target: flags.resume, explicit: false } : { kind: "select" } });
32
- if (flags.forkFrom) choices.push({ flag: "--fork", entry: { kind: "fork", target: flags.forkFrom } });
33
- if (flags.continueLatest) choices.push({ flag: "--continue", entry: { kind: "continue" } });
34
- // --fresh predates --no-session; together they still mean a fresh ephemeral log.
35
- if (flags.fresh || flags.noSession) choices.push({ flag: flags.noSession ? "--no-session" : "--fresh",
36
- entry: { kind: flags.noSession ? "ephemeral" : "new" } });
37
- if (choices.length > 1) throw new Error(`conflicting session options: ${choices.map((c) => c.flag).join(", ")}`);
38
- return choices[0]?.entry ?? { kind: "new" };
39
- }
40
-
41
- export interface SavedModel {
42
- model: string;
43
- provider?: string;
44
- }
45
-
46
- /**
47
- * S.3: every cursor of this session format names its provider, so a resume
48
- * restores exactly that provider/model and never guesses one from a model ID
49
- * that several providers serve.
50
- */
51
- export function restoreSessionModel<T extends { id: string; provider: string }>(
52
- saved: SavedModel,
53
- catalog: readonly T[],
54
- ): T {
55
- if (saved.provider === undefined) throw new Error(`saved model ${saved.model} names no provider, which this session format always records. Start a new session`);
56
- const matches = catalog.filter((m) => m.id === saved.model && m.provider === saved.provider);
57
- const label = `${saved.provider}/${saved.model}`;
58
- if (matches.length === 0) throw new Error(`saved model ${label} is unavailable. Restore its catalog entry, or start a new session`);
59
- if (matches.length !== 1) throw new Error(`saved model ${label} is ambiguous across providers. Restore an unambiguous catalog before resuming`);
60
- return matches[0]!;
61
- }
@@ -1,42 +0,0 @@
1
- /** D8 / D11: terminal exit is a bounded projection of execution facts, never a replay. */
2
- import type { LogEvent } from "./events.ts";
3
- import { pendingToolCalls } from "./execution.ts";
4
- import { modelOutcome } from "./outcome.ts";
5
- import { currentRequestEvents } from "./request-state.ts";
6
-
7
- export const DEFAULT_FULLSCREEN_EXIT_OUTPUT = "resume-hint";
8
-
9
- /** The identifiers `--session-id` accepts; anything else is printed as a path. */
10
- const PLAIN_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
11
-
12
- /**
13
- * The command a person retypes to come back. Claude Code prints
14
- * `claude --resume <id>` and Codex `codex resume <id>`: an id, not a path.
15
- * `--resume <id>` resolves only inside the sessions directory of the shell's
16
- * cwd, so a session stored anywhere else (`--session-dir`, `--state-dir`,
17
- * `-w` elsewhere) keeps the explicit path rather than a command that cannot
18
- * find it.
19
- */
20
- export function resumeCommand(session: { id: string; directory: string; path: string }, cwdSessions: string): string {
21
- return session.directory === cwdSessions && PLAIN_ID.test(session.id)
22
- ? `xeno --resume ${session.id}` : `xeno --session ${JSON.stringify(session.path)}`;
23
- }
24
-
25
- /** Terminal controls and arbitrarily large reason text do not own the shell surface. */
26
- export function exitLine(text: string, limit = 180): string {
27
- return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim().slice(0, limit);
28
- }
29
-
30
- export function sessionExitStatus(events: readonly LogEvent[]): string {
31
- const current = currentRequestEvents(events);
32
- const outcome = current.findLast(event => event.t === "outcome" || event.t === "user_bash");
33
- if (outcome?.t === "outcome") return exitLine(`${outcome.outcome.status} · ${outcome.outcome.reason} · ${outcome.reason}`);
34
- if (outcome?.t === "user_bash") return `${outcome.outcome.status} · ${outcome.outcome.reason} · command exit ${outcome.exit}`;
35
- if (current.some(event => event.t === "interrupt")) return "cancelled · user interrupted";
36
- const assistant = current.findLast(event => event.t === "assistant");
37
- if (assistant?.t === "assistant" && !assistant.blocks.some(block => block.type === "tool_use") && pendingToolCalls(current).length === 0) {
38
- const result = modelOutcome(assistant.stop_reason ?? null);
39
- return result.reason === "model_end" ? "model finished · verification remains in session evidence" : `${result.status} · ${result.reason}`;
40
- }
41
- return current.length === 0 ? "session saved" : "unfinished · inspect recorded receipts before continuing";
42
- }