@visual-z/xeno 0.8.4 → 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 (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,31 +0,0 @@
1
- /**
2
- * The current request's operations outside the workspace, read from the log
3
- * when a request ends (feat-external-effects-summary). The derivation is the
4
- * pure core function; this file only supplies what core may not read: the
5
- * home directory and the platform's temporary directory.
6
- * Nothing is recorded -- the list is a projection and is recomputed on demand.
7
- */
8
- import { realpathSync } from "node:fs";
9
- import { homedir, tmpdir } from "node:os";
10
- import { externalEffects, type ExternalEffect, type ExternalOptions } from "../core/external-effects.ts";
11
- import { currentRequestEvents } from "../core/request-state.ts";
12
- import type { Session } from "./loop.ts";
13
-
14
- /** macOS reports /var/folders/... while processes see /private/var/folders/...; both are temporary. */
15
- export function hostExternalOptions(): ExternalOptions {
16
- const temp = tmpdir();
17
- let real = temp;
18
- try { real = realpathSync(temp); } catch { /* an unreadable temp root is still the reported one */ }
19
- return { home: homedir(), temp: [...new Set([temp, real])] };
20
- }
21
-
22
- /**
23
- * Synchronous on purpose: SessionControl computes this between the turn's last
24
- * event and releasing the turn, and an await there lets a disposal observe a
25
- * turn that has already ended. The cost is one known gap: a request that
26
- * crossed a segment boundary lists only what ran after the last cut (D8 keeps
27
- * the earlier part in the sealed segment, not in memory). Silence, not a guess.
28
- */
29
- export function requestExternalEffects(session: Session, options: ExternalOptions = hostExternalOptions()): ExternalEffect[] {
30
- return externalEffects(currentRequestEvents(session.state.events), session.config.tools.root, options);
31
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Ask the npm registry for the newest published xeno (docs/tasks/feat-update-check.md).
3
- *
4
- * One small GET (the dist-tags document is about 20 bytes) with a 3-second
5
- * timeout, falling back to the package's `latest` document. It carries
6
- * Bun's default User-Agent and nothing that identifies the person: the
7
- * registry sees an IP address, the time and the package name. Failure of
8
- * any kind is "no answer"; callers decide whether an explicit check needs feedback.
9
- */
10
- import { distTagsUrl } from "../core/update-check.ts";
11
- import { parseVersion } from "../core/version.ts";
12
-
13
- /** How long to wait for the registry. Codex and pi wait longer; startup never waits on it anyway. Assumed. */
14
- export const UPDATE_TIMEOUT_MS = 3000;
15
-
16
- type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
17
-
18
- export interface LatestVersionOptions {
19
- /**
20
- * Ends the request early. The interactive app aborts it on close: an open
21
- * socket keeps the process alive, so quitting would otherwise wait out the
22
- * timeout (feat-update-check DoD 3).
23
- */
24
- readonly signal?: AbortSignal;
25
- readonly fetcher?: Fetch;
26
- readonly timeoutMs?: number;
27
- }
28
-
29
- export async function fetchLatestVersion(registry: string | undefined, options: LatestVersionOptions = {}): Promise<string | null> {
30
- const fetcher = options.fetcher ?? ((input, init) => fetch(input, init));
31
- const timeout = AbortSignal.timeout(options.timeoutMs ?? UPDATE_TIMEOUT_MS);
32
- const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
33
- const tags = distTagsUrl(registry);
34
- const attempts: [string, (body: Record<string, unknown>) => unknown][] = [
35
- [tags, body => body["latest"]],
36
- // Mirrors that do not serve dist-tags still serve the package's latest document.
37
- [tags.replace(/\/-\/package\/([^/]+)\/dist-tags$/, "/$1/latest"), body => body["version"]],
38
- ];
39
- for (const [url, pick] of attempts) {
40
- try {
41
- const response = await fetcher(url, { signal, headers: { accept: "application/json" } });
42
- if (!response.ok) continue;
43
- const value = pick(await response.json() as Record<string, unknown>);
44
- if (typeof value === "string" && value.length <= 64 && parseVersion(value)) return value.trim();
45
- } catch { if (signal.aborted) return null; }
46
- }
47
- return null;
48
- }
@@ -1,253 +0,0 @@
1
- /**
2
- * Per-request usage accounting.
3
- *
4
- * This exists to answer the question the whole design rests on: is the prompt
5
- * prefix actually stable? The core's claim is that appending within a node
6
- * only appends, so every turn should read the entire accumulated prefix from
7
- * cache and write only what the last turn added. That is checkable, and the
8
- * shape of a healthy loop is specific:
9
- *
10
- * cache_read grows turn over turn and covers the whole prior prefix
11
- * cache_creation is small -- only the delta
12
- * input_tokens (uncached) stays near zero after the first turn
13
- *
14
- * A cache_read that sits at zero across turns with the same prefix means a
15
- * silent invalidator got in. Since that failure costs money without ever
16
- * raising an error, measuring it is the only way to know.
17
- *
18
- * Pure: the aggregator takes samples and returns totals. The caller records
19
- * the timestamps.
20
- *
21
- * The field names below are a NORMALISED internal vocabulary, not one
22
- * provider's wire format. Every adapter maps onto them: a provider that bills
23
- * nothing for cache writes reports 0 there rather than leaving the field to
24
- * mean something else.
25
- */
26
-
27
- import type { CacheKind, CacheRates, Price } from "./model.ts";
28
-
29
- /** What one API response reports, normalised by the adapter. */
30
- export interface UsageSample {
31
- input_tokens: number;
32
- output_tokens: number;
33
- cache_creation_input_tokens: number;
34
- cache_read_input_tokens: number;
35
- /** Which node the request belonged to, so per-node costs stay separable. */
36
- node: number;
37
- /** Wall-clock milliseconds for the request. */
38
- ms: number;
39
- }
40
-
41
- export interface UsageTotals {
42
- requests: number;
43
- input_tokens: number;
44
- output_tokens: number;
45
- cache_creation_input_tokens: number;
46
- cache_read_input_tokens: number;
47
- ms: number;
48
- /**
49
- * Total prompt size is the sum of all three input fields -- `input_tokens`
50
- * alone is only the uncached remainder. An agent that ran for hours can show
51
- * 4K there while the real prefix was 200K.
52
- */
53
- prompt_tokens: number;
54
- /**
55
- * Fraction of prompt tokens served from cache. The number that says whether
56
- * the append-only property is holding in practice.
57
- */
58
- cache_hit_rate: number;
59
- }
60
-
61
- export function emptyTotals(): UsageTotals {
62
- return {
63
- requests: 0,
64
- input_tokens: 0,
65
- output_tokens: 0,
66
- cache_creation_input_tokens: 0,
67
- cache_read_input_tokens: 0,
68
- ms: 0,
69
- prompt_tokens: 0,
70
- cache_hit_rate: 0,
71
- };
72
- }
73
-
74
- export function totalUsage(samples: readonly UsageSample[]): UsageTotals {
75
- const t = emptyTotals();
76
- for (const s of samples) {
77
- t.requests += 1;
78
- t.input_tokens += s.input_tokens;
79
- t.output_tokens += s.output_tokens;
80
- t.cache_creation_input_tokens += s.cache_creation_input_tokens;
81
- t.cache_read_input_tokens += s.cache_read_input_tokens;
82
- t.ms += s.ms;
83
- }
84
- t.prompt_tokens =
85
- t.input_tokens + t.cache_creation_input_tokens + t.cache_read_input_tokens;
86
- t.cache_hit_rate =
87
- t.prompt_tokens === 0 ? 0 : t.cache_read_input_tokens / t.prompt_tokens;
88
- return t;
89
- }
90
-
91
- /**
92
- * Below this a request's speed is not shown: a response that returns at once
93
- * (a test double, a local cache) would divide into thousands of tokens per
94
- * second. Assumed, not measured.
95
- */
96
- export const SPEED_MIN_MS = 250;
97
-
98
- /**
99
- * Output tokens per second over a set of model requests, or undefined when
100
- * none can be measured (feat-sidebar-goal-and-speed). OpenCode's built-in
101
- * turnTokensPerSecond uses the same definition: output over the time the
102
- * requests took, each request counted once. Not output over the time since
103
- * the first visible output -- output counts include reasoning that providers
104
- * do not always stream (OpenAI summaries arrive late, Anthropic can omit
105
- * them), so that would keep reasoning tokens and drop reasoning time, and
106
- * overstate. Whole request time never overstates; an overloaded upstream
107
- * reads slow, which is what the user is waiting on.
108
- */
109
- export function generationSpeed(samples: readonly UsageSample[]): number | undefined {
110
- let tokens = 0, ms = 0;
111
- for (const sample of samples) {
112
- if (sample.output_tokens <= 0 || !(sample.ms >= SPEED_MIN_MS)) continue;
113
- tokens += sample.output_tokens;
114
- ms += sample.ms;
115
- }
116
- return ms > 0 ? tokens / (ms / 1000) : undefined;
117
- }
118
-
119
- /**
120
- * Cost in equivalent base-input tokens, the unit appendix A's model uses.
121
- *
122
- * Reported as a multiplier rather than dollars so it stays true across models
123
- * and price changes. The rates come from the model rather than from this
124
- * module: they are not a property of the unit, they are a property of the
125
- * provider's contract, and they differ by an order of magnitude. Anthropic
126
- * bills reads at 0.1x and 5-minute writes at 1.25x; DeepSeek bills reads at
127
- * 0.033x and writes at nothing. Output is counted separately because its rate
128
- * is a different multiple of input and varies by model.
129
- */
130
- export interface EquivalentCost {
131
- input_equivalent: number;
132
- output_tokens: number;
133
- /** What the same prompts would have cost with no caching at all. */
134
- uncached_equivalent: number;
135
- /** uncached / actual. 1.0 means caching bought nothing. */
136
- savings_ratio: number;
137
- }
138
-
139
- /**
140
- * Dollar cost of a session: complete, partial, or an explicit unknown.
141
- *
142
- * Unknown is a distinct member, not the number 0: a missing price times any
143
- * token count is not "this session was free", and collapsing the two is how
144
- * `/usage` would silently misreport every unpriced model.
145
- *
146
- * Partial is a distinct member, not `{kind:"usd", amount, note}`. A caller
147
- * that only reads `amount` on `usd` must not be able to print a total that
148
- * silently omitted cache spend as if the unknown rate were zero. `known`
149
- * is the part we can prove; `uncounted` is why it is not a complete bill.
150
- */
151
- export type ReportedCost =
152
- | { kind: "unknown" }
153
- | { kind: "usd"; amount: number }
154
- | { kind: "partial"; known: number; uncounted: "cache" };
155
-
156
- export function reportedCost(t: UsageTotals, price: Price | undefined): ReportedCost {
157
- if (price === undefined) return { kind: "unknown" };
158
- const known =
159
- (t.input_tokens * price.input + t.output_tokens * price.output) / 1_000_000;
160
- if (price.cacheRead !== undefined && price.cacheWrite !== undefined) {
161
- const amount =
162
- known +
163
- (t.cache_read_input_tokens * price.cacheRead +
164
- t.cache_creation_input_tokens * price.cacheWrite) /
165
- 1_000_000;
166
- return { kind: "usd", amount };
167
- }
168
- // One or both cache rates are missing. Add the ones we have; never
169
- // substitute 0 for the ones we do not — that is the low-bill path.
170
- let extra = 0;
171
- if (price.cacheRead !== undefined) {
172
- extra += (t.cache_read_input_tokens * price.cacheRead) / 1_000_000;
173
- }
174
- if (price.cacheWrite !== undefined) {
175
- extra += (t.cache_creation_input_tokens * price.cacheWrite) / 1_000_000;
176
- }
177
- return { kind: "partial", known: known + extra, uncounted: "cache" };
178
- }
179
-
180
- export function equivalentCost(t: UsageTotals, rates: CacheRates): EquivalentCost {
181
- const actual =
182
- t.input_tokens +
183
- t.cache_creation_input_tokens * rates.write +
184
- t.cache_read_input_tokens * rates.read;
185
- const uncached = t.prompt_tokens;
186
- return {
187
- input_equivalent: actual,
188
- output_tokens: t.output_tokens,
189
- uncached_equivalent: uncached,
190
- savings_ratio: actual === 0 ? 1 : uncached / actual,
191
- };
192
- }
193
-
194
- /**
195
- * Per-turn diagnosis of whether the prefix held.
196
- *
197
- * `cache_creation` above the delta means something ahead of the new content
198
- * changed, which is the signature of an invalidator rather than of growth.
199
- *
200
- * What counts as a symptom depends on who controls the cache. Under `explicit`
201
- * caching the caller placed the breakpoint, so a miss on a follow-up request
202
- * means the prefix moved and that is worth shouting about. Under `automatic`
203
- * caching the provider decides: DeepSeek documents its cache as best-effort
204
- * with no guaranteed hit rate and writes that "take seconds", so an early miss
205
- * is the normal case. Reporting it would light a warning at the start of every
206
- * session, and a warning that is always on stops being read.
207
- */
208
- export type PrefixVerdict =
209
- | { kind: "cold"; detail: string }
210
- | { kind: "healthy"; read: number; wrote: number }
211
- | { kind: "suspicious"; detail: string };
212
-
213
- export function diagnosePrefix(
214
- sample: UsageSample,
215
- previous: UsageSample | undefined,
216
- cache: CacheKind,
217
- ): PrefixVerdict {
218
- if (!previous) {
219
- return { kind: "cold", detail: "first request of the session; nothing to reuse yet" };
220
- }
221
- if (sample.cache_read_input_tokens === 0) {
222
- return cache === "automatic"
223
- ? {
224
- kind: "cold",
225
- detail:
226
- "no cache hit yet -- this provider caches on its own, best-effort, and " +
227
- "a write takes seconds to land. Only a problem if it persists across turns",
228
- }
229
- : {
230
- kind: "suspicious",
231
- detail:
232
- "cache_read is 0 on a follow-up request -- the prefix changed, or it is " +
233
- "below the model's minimum cacheable size",
234
- };
235
- }
236
- const previousPrompt =
237
- previous.input_tokens +
238
- previous.cache_creation_input_tokens +
239
- previous.cache_read_input_tokens;
240
- if (sample.cache_read_input_tokens < previousPrompt * 0.5) {
241
- return {
242
- kind: "suspicious",
243
- detail:
244
- `cache_read (${sample.cache_read_input_tokens}) covers less than half of the ` +
245
- `previous prompt (${previousPrompt}) -- part of the prefix was invalidated`,
246
- };
247
- }
248
- return {
249
- kind: "healthy",
250
- read: sample.cache_read_input_tokens,
251
- wrote: sample.cache_creation_input_tokens,
252
- };
253
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * The one place xeno's version is known (S.4: a release edits package.json and
3
- * nothing else). It is sent as the User-Agent (D18: xeno names itself
4
- * honestly) and compared against npm for update notices.
5
- *
6
- * Read at load rather than imported: `bun run check` requires every import to
7
- * be a literal relative `.ts` path. package.json ships in every install, so
8
- * this resolves the same way from a checkout and from a global install.
9
- */
10
- import { readFileSync } from "node:fs";
11
-
12
- export const XENO_VERSION: string = (JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")) as { version: string }).version;
@@ -1,56 +0,0 @@
1
- /** Shared bounded network leaves; request errors never include arbitrary peer text or credentials. */
2
- import { webUrl } from "../core/web-content.ts";
3
- import { READ_ATTEMPTED, ToolFault } from "../core/tool-failure.ts";
4
-
5
- // Assumed resource bounds follow OpenCode's 5 MiB response cap; streamed enforcement prevents oversized allocation.
6
- export const MAX_WEB_RESPONSE_BYTES = 5 * 1024 * 1024;
7
- export const MAX_WEB_REDIRECTS = 5;
8
- export type WebFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
9
- export async function boundedResponseBytes(response: Response, limit = MAX_WEB_RESPONSE_BYTES): Promise<Uint8Array> {
10
- const length = Number(response.headers.get("content-length"));
11
- if (Number.isFinite(length) && length > limit) {
12
- await response.body?.cancel(); throw new ToolFault("execution_failed", `Response exceeds ${limit} bytes`, READ_ATTEMPTED);
13
- }
14
- if (!response.body) return new Uint8Array();
15
- const reader = response.body.getReader(), chunks: Uint8Array[] = [];
16
- let size = 0;
17
- try {
18
- for (;;) {
19
- const next = await reader.read();
20
- if (next.done) break;
21
- size += next.value.byteLength;
22
- if (size > limit) { await reader.cancel(); throw new ToolFault("execution_failed", `Response exceeds ${limit} bytes`, READ_ATTEMPTED); }
23
- chunks.push(next.value);
24
- }
25
- } finally { reader.releaseLock(); }
26
- const bytes = new Uint8Array(size); let offset = 0;
27
- for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
28
- return bytes;
29
- }
30
- export async function boundedWebRequest(url: string, init: RequestInit, timeoutMs: number, signal: AbortSignal | undefined,
31
- fetcher: WebFetch = fetch, redirects = MAX_WEB_REDIRECTS): Promise<{ response: Response; bytes: Uint8Array; url: string; redirects: string[] }> {
32
- const deadline = new AbortController(), combined = AbortSignal.any([deadline.signal, ...(signal ? [signal] : [])]);
33
- const timer = setTimeout(() => deadline.abort(), timeoutMs), history: string[] = [];
34
- let target = webUrl(url);
35
- try {
36
- combined.throwIfAborted();
37
- for (;;) {
38
- const response = await fetcher(target, { ...init, redirect: "manual", signal: combined });
39
- if ([301, 302, 303, 307, 308].includes(response.status)) {
40
- await response.body?.cancel();
41
- const location = response.headers.get("location");
42
- if (!location || history.length >= redirects) throw new ToolFault("execution_failed", "Redirect limit exceeded or Location missing", READ_ATTEMPTED);
43
- const next = webUrl(new URL(location, target).href);
44
- if (target.startsWith("https:") && next.startsWith("http:")) throw new ToolFault("execution_failed", "HTTPS redirects cannot downgrade to HTTP", READ_ATTEMPTED);
45
- history.push(target); target = next;
46
- continue;
47
- }
48
- return { response, bytes: await boundedResponseBytes(response), url: target, redirects: history };
49
- }
50
- } catch (error) {
51
- if (signal?.aborted) throw new ToolFault("cancelled", "Network request cancelled", READ_ATTEMPTED);
52
- if (deadline.signal.aborted) throw new ToolFault("timeout", "Network request timed out", READ_ATTEMPTED);
53
- if (error instanceof ToolFault) throw error;
54
- throw new ToolFault("execution_failed", "Network request failed; verify the URL and network connection", READ_ATTEMPTED);
55
- } finally { clearTimeout(timer); }
56
- }
@@ -1,88 +0,0 @@
1
- /** Network adapters share the normal tool admission and append-only observation path (D11/D13). */
2
- import { join } from "node:path";
3
- import { webFetchArguments, webSearchArguments, webSearchHits, webSearchProjection, decodeWebEntities,
4
- type WebFormat, type WebSearchConfig } from "../core/web-content.ts";
5
- import { ToolFault, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
6
- import { archiveObservation } from "../store/observations.ts";
7
- import { presentObservation, type ToolContext, type ToolDefinition } from "./tools.ts";
8
- import { boundedWebRequest, type WebFetch } from "./web-http.ts";
9
-
10
- /** Bun's native HTML parser handles malformed/quoted markup; no homegrown HTML tokeniser or new dependency. */
11
- export async function extractWebHtml(html: string, url: string, format: Exclude<WebFormat, "html">): Promise<{ title: string; content: string }> {
12
- let title = "";
13
- const cleaned = await new HTMLRewriter().on("title", { text(chunk) { title += chunk.text; } })
14
- .on("head,script,style,noscript,iframe,object,embed,template", { element(element) { element.remove(); } })
15
- .transform(new Response(html)).text();
16
- const blocks = new Set(["p", "div", "section", "article", "main", "header", "footer", "blockquote", "ul", "ol", "table", "tr", "pre", "h1", "h2", "h3", "h4", "h5", "h6"]);
17
- const content = await new HTMLRewriter().on("*", { element(element) {
18
- const tag = element.tagName;
19
- if (blocks.has(tag) || tag === "br" || tag === "li") element.before("\n");
20
- if (format === "markdown" && /^h[1-6]$/.test(tag)) element.before("#".repeat(Number(tag[1])) + " ");
21
- if (format === "markdown" && tag === "li") element.before("- ");
22
- if (format === "markdown" && tag === "pre") element.before("```\n");
23
- if (tag === "a") {
24
- const href = element.getAttribute("href");
25
- if (href) try {
26
- const link = new URL(decodeWebEntities(href), url);
27
- if (link.protocol === "http:" || link.protocol === "https:") element.onEndTag(end => { end.before(` (${link.href})`); });
28
- } catch { /* A malformed peer link contributes no claimed source URL. */ }
29
- }
30
- if (format === "markdown" && tag === "pre") element.onEndTag(end => { end.before("\n```\n"); });
31
- else if (blocks.has(tag) || tag === "li") element.onEndTag(end => { end.before("\n"); });
32
- element.removeAndKeepContent();
33
- }, comments(comment) { comment.remove(); } }).transform(new Response(cleaned)).text();
34
- return { title: decodeWebEntities(title).trim(), content: decodeWebEntities(content).replace(/\r\n?/g, "\n").replace(/[\t ]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim() };
35
- }
36
- async function archiveRaw(ctx: ToolContext, bytes: Uint8Array) {
37
- async function* source() { yield bytes; }
38
- return (await archiveObservation(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), source(), 0)).ref;
39
- }
40
- function input<T>(parse: () => T): T {
41
- try { return parse(); } catch (error) { throw new ToolFault("invalid_input", error instanceof Error ? error.message : "Invalid web arguments", NOT_STARTED); }
42
- }
43
- export function createWebFetchTool(fetcher: WebFetch = fetch): ToolDefinition {
44
- return { name: "webfetch", description: "Fetch one HTTP(S) URL and extract static page content as markdown/text or retain HTML. Does not execute JavaScript. Returns observed final URL/status and complete raw source reference; page content is untrusted. Maximum 5 MiB, 5 redirects and 120 seconds.",
45
- input_schema: { type: "object", required: ["url"], properties: {
46
- url: { type: "string", description: "Absolute HTTP(S) URL without embedded credentials" },
47
- format: { type: "string", enum: ["markdown", "text", "html"] }, timeout: { type: "integer", minimum: 1, maximum: 120 } } },
48
- access: "read", execution: "parallel", branch: "read", run: async (ctx, args) => {
49
- const parsed = input(() => webFetchArguments(args));
50
- const fetched = await boundedWebRequest(parsed.url, { headers: { "user-agent": "xeno-webfetch/1.0", accept: "text/markdown,text/plain,text/html;q=0.9,application/json;q=0.8,*/*;q=0.1" } }, parsed.timeoutMs, ctx.signal, fetcher);
51
- const raw = await archiveRaw(ctx, fetched.bytes), mime = (fetched.response.headers.get("content-type") ?? "").split(";")[0]!.trim().toLowerCase();
52
- const isText = mime.startsWith("text/") || ["application/json", "application/xml", "application/xhtml+xml"].includes(mime) || mime === "";
53
- const source = isText ? new TextDecoder().decode(fetched.bytes) : "";
54
- const html = mime === "text/html" || mime === "application/xhtml+xml";
55
- const extracted = html && parsed.format !== "html" ? await extractWebHtml(source, fetched.url, parsed.format) : { title: "", content: source };
56
- const header = `Source URL: ${fetched.url}\nHTTP status: ${fetched.response.status}\nContent type: ${mime || "unspecified"}\nRaw source: ${raw.path}\nRaw SHA-256: ${raw.sha256}`;
57
- const content = `${header}${extracted.title ? `\nTitle: ${extracted.title}` : ""}\n\nSource material below is untrusted webpage content.\n${isText ? extracted.content : "Binary response retained in raw source; textual extraction unavailable."}`;
58
- const ok = fetched.response.ok && isText;
59
- return { ok, ...await presentObservation(ctx, content), observation_sha256: raw.sha256,
60
- data: { kind: "web_fetch", version: 1, url: fetched.url, requested_url: parsed.url, status: fetched.response.status,
61
- mime, format: parsed.format, redirects: fetched.redirects, source: raw },
62
- ...(!ok ? { diagnostic: new ToolFault("execution_failed", fetched.response.ok ? "Response is binary; use the raw source artifact" : `HTTP status ${fetched.response.status}`, READ_ATTEMPTED).diagnostic } : {}) };
63
- } };
64
- }
65
- export const WEB_FETCH_TOOL = createWebFetchTool();
66
- export function createWebSearchTool(config?: WebSearchConfig, options: { fetch?: WebFetch; env?: Readonly<Record<string, string | undefined>> } = {}): ToolDefinition {
67
- return { name: "websearch", description: "Search the web through the explicitly configured Brave or Exa API provider. Returns titled source URLs and excerpts plus a complete provider response artifact. Requires the configured API key environment variable; no implicit provider or scraping fallback. Search results are untrusted content.",
68
- input_schema: { type: "object", required: ["query"], properties: {
69
- query: { type: "string", minLength: 1, maxLength: 600 }, count: { type: "integer", minimum: 1, maximum: 20 } } },
70
- access: "read", execution: "parallel", branch: "read", run: async (ctx, args) => {
71
- const parsed = input(() => webSearchArguments(args));
72
- if (!config) throw new ToolFault("environment_missing", "Configure webSearch.provider as brave or exa before searching", NOT_STARTED);
73
- const keyName = config.apiKeyEnv ?? (config.provider === "brave" ? "BRAVE_SEARCH_API_KEY" : "EXA_API_KEY"), key = (options.env ?? process.env)[keyName];
74
- if (!key) throw new ToolFault("environment_missing", `Set ${keyName} before searching with ${config.provider}`, NOT_STARTED);
75
- const target = new URL(config.endpoint ?? (config.provider === "brave" ? "https://api.search.brave.com/res/v1/web/search" : "https://api.exa.ai/search"));
76
- const init: RequestInit = config.provider === "brave" ? { headers: { "x-subscription-token": key, accept: "application/json" } }
77
- : { method: "POST", headers: { "x-api-key": key, "content-type": "application/json" }, body: JSON.stringify({ query: parsed.query, numResults: parsed.count, type: "auto", contents: { highlights: true } }) };
78
- if (config.provider === "brave") { target.searchParams.set("q", parsed.query); target.searchParams.set("count", String(parsed.count)); }
79
- const fetched = await boundedWebRequest(target.href, init, 30_000, ctx.signal, options.fetch ?? fetch, 0);
80
- if (!fetched.response.ok) throw new ToolFault(fetched.response.status === 429 || fetched.response.status >= 500 ? "transient" : "execution_failed", `Search provider returned HTTP ${fetched.response.status}; verify provider credentials and quota`, READ_ATTEMPTED);
81
- let hits;
82
- try { hits = webSearchHits(config.provider, JSON.parse(new TextDecoder().decode(fetched.bytes)), parsed.count); }
83
- catch { throw new ToolFault("execution_failed", "Search provider response failed schema validation", READ_ATTEMPTED); }
84
- const raw = await archiveRaw(ctx, fetched.bytes);
85
- return { ok: true, ...await presentObservation(ctx, webSearchProjection(config.provider, parsed.query, hits) + `\n\nProvider response: ${raw.path}\nSHA-256: ${raw.sha256}`),
86
- observation_sha256: raw.sha256, data: { kind: "web_search", version: 1, provider: config.provider, query: parsed.query, results: hits.map(hit => ({ ...hit })), source: raw } };
87
- } };
88
- }
@@ -1,60 +0,0 @@
1
- /** O.2/O.6: validate annotations against the full projection before they can be appended. */
2
- import type { LogEvent } from "../core/events.ts";
3
- import { deriveHandoff } from "../core/derive.ts";
4
- import { scopedWorkId, workChangeError } from "../core/work-state.ts";
5
- import { resolveEvidence, type EvidenceResolution } from "../core/evidence.ts";
6
- import { unknownWorkClaimDiagnostic, unverifiedNoteDiagnostic } from "../core/note-guidance.ts";
7
- import { failureDiagnostic, NOT_STARTED } from "../core/tool-failure.ts";
8
- import type { ToolDiagnostic } from "../core/tool-contract.ts";
9
- import { replayContext } from "../store/replay.ts";
10
- import { digestNode, nodePath } from "../store/log.ts";
11
- import { resolveEvidenceNote } from "./evidence-notes.ts";
12
- import type { ToolResult } from "./tools.ts";
13
-
14
- function rejected(message: string, diagnostic = failureDiagnostic("invalid_input", message, NOT_STARTED)): { out: ToolResult } {
15
- return { out: { ok: false, content: message, data: { recorded: false }, diagnostic } };
16
- }
17
- function claimResult(annotation: Extract<LogEvent, { t: "claim" }>, proof: EvidenceResolution, openedAt?: string): ToolResult {
18
- const diagnostic: ToolDiagnostic | undefined = proof.ok ? undefined : unverifiedNoteDiagnostic(proof, annotation.id, openedAt);
19
- return { ok: proof.ok, content: JSON.stringify({ recorded: true, claim_event: annotation.id,
20
- verification: proof.ok ? "verified" : "unverified", ...(proof.ok ? { evidence: proof.evidence } : { reason: proof.reason }) }),
21
- ...(diagnostic ? { diagnostic } : {}) };
22
- }
23
-
24
- export async function prepareWorkingNote(logDir: string, node: number, namespace: string, annotation: LogEvent | undefined, out: ToolResult): Promise<{ out: ToolResult; annotation?: LogEvent }> {
25
- if (!annotation) return { out };
26
- if (annotation.t !== "claim" && annotation.t !== "evidence_link" && !("work_id" in annotation && annotation.work_id !== undefined)) return { out, annotation };
27
- const replayed = await replayContext(logDir, node);
28
- const input = { prev: replayed.fullHandoff, events: replayed.events, depth: replayed.depth,
29
- self: { node, log_ref: nodePath(logDir, node), sha256: await digestNode(logDir, node) } };
30
- const current = deriveHandoff(input);
31
- if (annotation.t === "evidence_link") {
32
- const resolved = await resolveEvidenceNote(logDir, node, namespace, current, annotation);
33
- return { out: resolved.result, ...(resolved.event ? { annotation: resolved.event } : {}) };
34
- }
35
- if (annotation.t === "claim" && annotation.work_id === undefined) {
36
- return { annotation, out: claimResult(annotation, resolveEvidence(input.events, annotation.evidence)) };
37
- }
38
- if (!("work_id" in annotation) || annotation.work_id === undefined) throw new Error("missing identified work annotation");
39
- const workId = scopedWorkId(namespace, annotation.work_id, current.state.work_items ?? []);
40
- annotation = { ...annotation, work_id: workId };
41
- const owned = current.state.work_items?.find(item => item.id === workId);
42
- if (owned?.owner && owned.owner !== namespace) return rejected(`work ${workId} is owned by ${owned.owner}`);
43
- if ((annotation.t === "next_action" || annotation.t === "work_dependencies") && annotation.depends_on !== undefined) annotation = {
44
- ...annotation, depends_on: annotation.depends_on.map(id => scopedWorkId(namespace, id, current.state.work_items ?? [])),
45
- };
46
- if ((annotation.t === "decision" || annotation.t === "pitfall") && !current.state.work_items?.some(item => item.id === workId)) {
47
- return rejected(`unknown work_id ${workId}; record a next note first`);
48
- }
49
- if (!owned && annotation.t === "claim") {
50
- const diagnostic = unknownWorkClaimDiagnostic(input.events, annotation);
51
- return rejected(diagnostic.message, diagnostic);
52
- }
53
- const error = workChangeError(current.state.work_items ?? [], annotation);
54
- if (error) return rejected(error);
55
- const proposed = deriveHandoff({ ...input, events: [...input.events, annotation] });
56
- const item = proposed.state.work_items?.find(item => item.id === workId);
57
- const claim = annotation.t === "claim" ? claimResult(annotation, resolveEvidence(input.events, annotation.evidence, owned?.opened_at), owned?.opened_at) : undefined;
58
- return { annotation, out: { ...claim, ok: annotation.t !== "claim" || item?.verification === "verified",
59
- content: JSON.stringify({ recorded: true, work_item: item }) } };
60
- }