@visual-z/xeno 0.8.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,479 +0,0 @@
1
- /**
2
- * Handoff: the structured projection that carries a segment's durable state into
3
- * the next one. Generated deterministically from the log with zero LLM calls
4
- * (decisions D2, D3).
5
- *
6
- * The field set borrows names from the closed MCP SEP #2683 proposal
7
- * (handoff_version, completed, decisions, pitfalls) for future interop. That
8
- * proposal states no types, no required fields and no invariants, so everything
9
- * below is this project's own -- in particular the invariants in
10
- * validateHandoff(), which are the part that actually does work.
11
- *
12
- * The contract is enforced here rather than in the prompt because the harness is
13
- * the only layer that sees prompt assembly, tool mediation and lifecycle events
14
- * at once -- the placement argument from ClawVM (arXiv:2604.10352).
15
- */
16
-
17
- import type { LineRange, Sha256, NodeRef, IsoTime, EvidenceQuery, EventId } from "./events.ts";
18
- import type { ProgressEvidence } from "./progress.ts";
19
- import { isExecutionOutcome, type ExecutionOutcome } from "./outcome.ts";
20
- import type { WorkItem } from "./work-state.ts";
21
- import type { EvidenceLink } from "./evidence-links.ts";
22
- import type { WorkingSet } from "./working-set.ts";
23
- import { RECOVERY_SECTIONS, RECOVERY_KINDS } from "./recovery-sources.ts";
24
- import type { AcceptanceState } from "./acceptance.ts";
25
- import type { LoadedSkill } from "./skills.ts";
26
- import type { ProcessObservation } from "./process-state.ts";
27
- import { emitYaml } from "./yaml.ts";
28
-
29
- export const HANDOFF_VERSION = 1 as const;
30
-
31
- /** Resolved proof that a completion claim is real: a log event that exited zero. */
32
- export type Evidence = {
33
- /** Configured gate name, when the backing command had one. */
34
- gate?: string;
35
- command: string;
36
- exit: number;
37
- at: IsoTime;
38
- /** Id of the backing tool_result event, so the claim is traceable to the log. */
39
- event_id: string;
40
- /** Exact observed workspace after the command; absence means legacy/unknown. */
41
- workspace_sha256?: string;
42
- /** P.3: omitted on historical full-workspace observations. */
43
- workspace_scope?: "source-v1";
44
- }
45
-
46
- export type CompletedItem = {
47
- what: string;
48
- work_id?: string;
49
- /** The proof as it stood when the claim was made. */
50
- evidence: Evidence;
51
- evidence_stale?: true;
52
- /**
53
- * Set when the gate this item cited is failing as of the latest run.
54
- *
55
- * The item stays in `completed` because it really did pass when claimed, and
56
- * deleting that record would throw away something true. But a reader scanning
57
- * `completed` would otherwise have to cross-reference `verification` to notice
58
- * the suite is now red, so the signal is repeated inline where it is read.
59
- */
60
- evidence_superseded?: { gate: string; status: "fail"; exit?: number; at?: IsoTime };
61
- }
62
-
63
- /**
64
- * A claim whose evidence did not resolve. Kept visible on purpose: a skipped or
65
- * failed check must never read as a pass, and silently dropping the claim would
66
- * hide exactly the information a reviewer needs.
67
- */
68
- export type UnverifiedClaim = {
69
- what: string;
70
- claimed: EvidenceQuery;
71
- reason: "no_matching_event" | "nonzero_exit" | "stale_workspace";
72
- /** Present when a matching event existed but failed. */
73
- exit?: number;
74
- }
75
-
76
- export type InFlightItem = {
77
- work_id?: string;
78
- what: string;
79
- next_action: string;
80
- }
81
-
82
- export type BlockedItem = {
83
- work_id?: string;
84
- what: string;
85
- reason: string;
86
- }
87
-
88
- export type DecisionItem = {
89
- work_id?: string;
90
- source?: { node: number; event_id: string };
91
- chose: string;
92
- because: string;
93
- rejected?: readonly string[];
94
- }
95
-
96
- export type PitfallItem = {
97
- work_id?: string;
98
- source?: { node: number; event_id: string };
99
- tried: string;
100
- failed_because: string;
101
- }
102
-
103
- export type FileAnchor = {
104
- path: string;
105
- /** Checked against subsequent workspace snapshots; true marks a mismatch. */
106
- sha256: Sha256;
107
- stale?: true;
108
- read_ranges?: readonly LineRange[];
109
- touched: readonly ("read" | "edit")[];
110
- }
111
-
112
- /**
113
- * A branch this node opened, and what became of it.
114
- *
115
- * Recorded in the handoff rather than only in the log because the handoff is
116
- * the only thing that crosses a boundary: a branch absent from it is a branch
117
- * the next context does not know exists.
118
- */
119
- export type BranchRecord = {
120
- node: number;
121
- /** Event id in this node where the branch was spawned. */
122
- spawned_at: EventId;
123
- purpose: string;
124
- background?: true;
125
- /** null means unresolved, which D16 requires be visible rather than quiet. */
126
- resolution: "merged" | "discarded" | null;
127
- /** Present once the branch joined; a path, never the handoff's contents. */
128
- handoff_ref?: string;
129
- summary?: string;
130
- receipt_ref?: string;
131
- outcome?: ExecutionOutcome;
132
- };
133
-
134
- export type VerificationStatus = "pass" | "fail" | "not_run";
135
-
136
- export type VerificationItem = {
137
- gate: string;
138
- status: VerificationStatus;
139
- at?: IsoTime;
140
- exit?: number;
141
- workspace_sha256?: string;
142
- workspace_scope?: "source-v1";
143
- stale?: true;
144
- }
145
-
146
- export type UserTurnRecord = {
147
- at: IsoTime;
148
- text: string;
149
- /**
150
- * Set when a later interrupt event cancelled this turn. Omitted rather
151
- * than false: absence means the turn was not abandoned, which is the
152
- * common case, and deleting the turn would rewrite history.
153
- */
154
- abandoned?: true;
155
- /**
156
- * Working set v3 only: the original user event, present exactly when the turn
157
- * carried images, so `history(event=ID)` can reattach them.
158
- */
159
- event?: EventId;
160
- /**
161
- * Working set v3 only: references to the turn's attached images (D8: digest
162
- * and identity, never bytes). Absent from the full derived state, so legacy
163
- * projections and frozen replay are unchanged.
164
- */
165
- images?: readonly UserTurnImage[];
166
- };
167
-
168
- /** Enough for the model to know an image exists and which one; bytes stay in the blob store. */
169
- export type UserTurnImage = { sha256: Sha256; name: string; width: number; height: number };
170
-
171
- export type Handoff = {
172
- handoff_version: typeof HANDOFF_VERSION | 2;
173
- working_set?: WorkingSet;
174
- acceptance?: AcceptanceState;
175
- loaded_skills?: readonly LoadedSkill[];
176
- /** O.14: last observations only; live capabilities cannot survive a restart. */
177
- processes?: readonly ProcessObservation[];
178
- /** V2 arrays are carried suffixes; the immutable source rebuilds every page. */
179
- history?: {
180
- source: NodeRef;
181
- sections: Record<string, { total: number; offset: number }>;
182
- };
183
- /**
184
- * The node this state was derived FROM.
185
- *
186
- * There is deliberately no field naming the node it is FOR: a fork makes one
187
- * handoff the basis for several children at once, so that question has no
188
- * single answer. Identity lives here and nowhere else.
189
- */
190
- predecessor: NodeRef | null;
191
-
192
- /**
193
- * Verbatim from the first user turn of segment 0 that was not later
194
- * cancelled. Never rewritten. If every user turn in that segment was
195
- * abandoned, this is the last of them (EMPTY_OBJECTIVE forbids blanking it);
196
- * the abandoned mark lives on `user_turns`.
197
- */
198
- objective: string;
199
- /**
200
- * Every user turn, verbatim, accumulated across segments. Which sentence is a
201
- * hard constraint is not derivable, and user turns are both the smallest and
202
- * highest-value content in a session -- so all of them are kept rather than
203
- * asking the model to label some of them.
204
- */
205
- user_turns: readonly UserTurnRecord[];
206
- /** Set only when the cap dropped turns; names the log refs that still hold them. */
207
- user_turns_truncated?: { dropped: number; recover_from: readonly string[] };
208
-
209
- state: {
210
- /** O.2: canonical identified work; legacy lists below are compatible projections. */
211
- work_items?: readonly WorkItem[];
212
- completed: readonly CompletedItem[];
213
- unverified_claims: readonly UnverifiedClaim[];
214
- in_flight: readonly InFlightItem[];
215
- blocked: readonly BlockedItem[];
216
- /** Derived from the log, so always present even when the model annotated nothing. */
217
- last_activity: string | null;
218
- };
219
-
220
- decisions: readonly DecisionItem[];
221
- pitfalls: readonly PitfallItem[];
222
- file_anchors: readonly FileAnchor[];
223
- verification: readonly VerificationItem[];
224
- /** Branches opened from this node, resolved or not. */
225
- branches: readonly BranchRecord[];
226
- open_questions: readonly string[];
227
- /** D9 exact novelty ledger. Absence is legacy; rebuild it from original events. */
228
- progress_evidence?: readonly ProgressEvidence[];
229
- /** O.6: original evidence references and replacement relationships, never inline history. */
230
- evidence_links?: readonly EvidenceLink[];
231
-
232
- /**
233
- * True only when work remains AND some in-flight item has a concrete next
234
- * action. Blocked-with-no-next-action is therefore invalid by construction,
235
- * which is the correct reading: a human has to look at it.
236
- */
237
- continuation_valid: boolean;
238
-
239
- budget: {
240
- /** Length of the path from the root to this node, inclusive. */
241
- nodes_on_path: number;
242
- /** Consecutive nodes along this path with no verifiable progress (D9). */
243
- nodes_without_progress: number;
244
- };
245
- }
246
-
247
- // ---------------------------------------------------------------------------
248
- // YAML field order: schema-defined, not alphabetical, so the file reads top-down
249
- // the way a person would want to read it (decision D10).
250
- // ---------------------------------------------------------------------------
251
-
252
- const ORDER: Record<string, readonly string[]> = {
253
- "": [
254
- "handoff_version", "predecessor", "history",
255
- "objective", "working_set", "user_turns", "user_turns_truncated",
256
- "state", "acceptance", "loaded_skills", "processes", "decisions", "pitfalls", "file_anchors",
257
- "verification", "branches", "open_questions", "progress_evidence", "evidence_links",
258
- "continuation_valid", "budget",
259
- ],
260
- predecessor: ["node", "log_ref", "sha256"],
261
- user_turns: ["at", "text", "abandoned", "event", "images"],
262
- "user_turns.images": ["sha256", "name", "width", "height"],
263
- user_turns_truncated: ["dropped", "recover_from"],
264
- state: ["completed", "unverified_claims", "in_flight", "blocked", "last_activity", "work_items"],
265
- "state.completed": ["work_id", "what", "evidence", "evidence_superseded"],
266
- "state.completed.evidence_superseded": ["gate", "status", "exit", "at"],
267
- "state.completed.evidence": ["gate", "command", "exit", "at", "event_id"],
268
- "state.unverified_claims": ["what", "claimed", "reason", "exit"],
269
- "state.in_flight": ["work_id", "what", "next_action"],
270
- "state.blocked": ["work_id", "what", "reason"],
271
- "state.work_items": ["id", "what", "status", "verification", "opened_at", "updated_at", "next_action", "reason", "evidence", "previous_evidence"],
272
- decisions: ["chose", "because", "rejected"],
273
- pitfalls: ["tried", "failed_because"],
274
- file_anchors: ["path", "sha256", "read_ranges", "touched"],
275
- verification: ["gate", "status", "at", "exit"],
276
- branches: ["node", "spawned_at", "purpose", "resolution", "handoff_ref", "summary", "outcome", "receipt_ref", "background"],
277
- processes: ["session", "node", "event", "observed", "command", "exit", "output"],
278
- "processes.output": ["sha256", "path", "bytes"],
279
- "branches.outcome": ["status", "reason"],
280
- evidence_links: ["id", "work", "what", "event", "resource", "version", "freshness", "superseded_by"],
281
- budget: ["nodes_on_path", "nodes_without_progress"],
282
- };
283
-
284
- /** Key-order resolver for emitYaml(). Array indices are transparent to the path. */
285
- export function handoffKeyOrder(path: readonly string[]): readonly string[] | undefined {
286
- return ORDER[path.join(".")];
287
- }
288
-
289
- /**
290
- * The one way to serialize a handoff.
291
- *
292
- * Callers cannot forget the key order and silently get alphabetical output. The
293
- * types above are declared as type aliases rather than interfaces so they carry
294
- * an implicit index signature and satisfy YamlValue directly -- no cast.
295
- */
296
- export function emitHandoff(h: Handoff): string {
297
- return emitYaml(h, { keyOrder: handoffKeyOrder });
298
- }
299
-
300
- // ---------------------------------------------------------------------------
301
- // Invariants
302
- // ---------------------------------------------------------------------------
303
-
304
- export type Violation = {
305
- code:
306
- | "COMPLETED_WITHOUT_PASSING_EVIDENCE"
307
- | "PASS_WITHOUT_ZERO_EXIT"
308
- | "ANCHOR_WITHOUT_SHA"
309
- | "EMPTY_OBJECTIVE"
310
- | "PREDECESSOR_MISSING"
311
- | "PREDECESSOR_WITHOUT_SHA"
312
- | "EMPTY_PATH"
313
- | "CONTINUATION_WITHOUT_NEXT_ACTION"
314
- | "INVALID_HISTORY"
315
- | "INVALID_EXECUTION_OUTCOME"
316
- | "INVALID_WORK_STATE"
317
- | "INVALID_EVIDENCE_LINK"
318
- | "INVALID_WORKING_SET"
319
- | "INVALID_USER_TURN"
320
- | "INVALID_ACCEPTANCE"
321
- | "INVALID_SKILL_SOURCE"
322
- | "INVALID_PROCESS_STATE"
323
- | "BAD_VERSION";
324
- detail: string;
325
- }
326
-
327
- /**
328
- * The enforceable contract. Every rule here exists because some real harness got
329
- * it wrong: "migration applied successfully" with a nonzero exit code nobody
330
- * read; a skipped check reported as a pass; a stale file snapshot presented as
331
- * current state.
332
- */
333
- export function validateHandoff(h: Handoff): Violation[] {
334
- const v: Violation[] = [];
335
- for (const link of h.evidence_links ?? []) if (!link.id || !link.work || !link.event ||
336
- !["current", "stale", "unknown"].includes(link.freshness) || (link.freshness !== "unknown" && !link.version)) {
337
- v.push({ code: "INVALID_EVIDENCE_LINK", detail: `invalid evidence reference ${link.id}` });
338
- }
339
- const workIds = new Set<string>();
340
- for (const item of h.state.work_items ?? []) {
341
- if (item.depends_on && (new Set(item.depends_on).size !== item.depends_on.length || item.depends_on.includes(item.id) ||
342
- item.depends_on.some(dep => !dep.trim())) || item.owner !== undefined && (!item.owner.trim() || !item.owner_event)) {
343
- v.push({ code: "INVALID_WORK_STATE", detail: `invalid dependencies or ownership for work ${item.id}` });
344
- }
345
- const proven = item.verification === "verified" || item.verification === "stale";
346
- if (!item.id.trim() || workIds.has(item.id) || !item.what.trim() || !item.opened_at || !item.updated_at ||
347
- !["active", "blocked", "completed", "cancelled"].includes(item.status) ||
348
- !["not_run", "verified", "unverified", "stale"].includes(item.verification) ||
349
- (item.status === "active" && !item.next_action?.trim()) ||
350
- (item.status === "completed" && !proven) ||
351
- (proven && (!item.evidence || item.evidence.exit !== 0)) ||
352
- (!proven && item.evidence !== undefined)) {
353
- v.push({ code: "INVALID_WORK_STATE", detail: `invalid identity, lifecycle or evidence for work ${item.id}` });
354
- }
355
- workIds.add(item.id);
356
- }
357
- for (const branch of h.branches) {
358
- if (branch.outcome !== undefined && !isExecutionOutcome(branch.outcome)) {
359
- v.push({ code: "INVALID_EXECUTION_OUTCOME", detail: `branch ${branch.node} has an invalid execution outcome` });
360
- }
361
- }
362
-
363
- if (h.handoff_version !== HANDOFF_VERSION && h.handoff_version !== 2) {
364
- v.push({ code: "BAD_VERSION", detail: `expected ${HANDOFF_VERSION}, got ${h.handoff_version}` });
365
- }
366
- if (h.working_set) {
367
- const view = h.working_set;
368
- if (![1, 2, 3].includes(view.version) || view.source.node !== h.predecessor?.node || view.source.sha256 !== h.predecessor?.sha256 ||
369
- !Number.isSafeInteger(view.omitted.work) || view.omitted.work < 0 || !Number.isSafeInteger(view.omitted.references) || view.omitted.references < 0 ||
370
- new Set(view.work.map(item => item.id)).size !== view.work.length || view.work.some(item => !["active", "blocked"].includes(item.status)) ||
371
- view.references.some(reference => !reference.event) ||
372
- view.version >= 2 && (!view.recovery_sections || RECOVERY_SECTIONS.some(section => {
373
- const range = view.recovery_sections![section], references = view.references.filter(ref => ref.kind === RECOVERY_KINDS[section]);
374
- return !range || !Number.isSafeInteger(range.total) || !Number.isSafeInteger(range.omitted) || range.omitted < 0 || range.omitted > range.total ||
375
- range.total - range.omitted !== references.length || new Set(references.map(ref => ref.event)).size !== references.length ||
376
- references.some(ref => !Number.isSafeInteger(ref.node) || Number(ref.node) < 0 || ref.provenance !==
377
- (section === "users" ? "user_message" : section === "next_actions" ? "model_annotation" : "observation"));
378
- }))) {
379
- v.push({ code: "INVALID_WORKING_SET", detail: "working set must reference its source and explicit active state" });
380
- }
381
- }
382
- // D8: an image reference must name its blob and the original event that reattaches it.
383
- if (h.user_turns.some(turn => turn.event !== undefined && !turn.images?.length ||
384
- turn.images !== undefined && (!turn.event || turn.images.length === 0 || turn.images.some(image => !/^[a-f0-9]{64}$/.test(image.sha256))))) {
385
- v.push({ code: "INVALID_USER_TURN", detail: "user turn images need their original event and blob digests" });
386
- }
387
- if (h.handoff_version === 2) {
388
- const history = h.history;
389
- if (!history || !h.predecessor || history.source.node !== h.predecessor.node ||
390
- history.source.sha256 !== h.predecessor.sha256) {
391
- v.push({ code: "INVALID_HISTORY", detail: "paged handoff must identify its immutable predecessor" });
392
- } else for (const [section, range] of Object.entries(history.sections)) {
393
- const values = section === "acceptance.criteria" ? h.acceptance?.criteria : section === "acceptance.targets" ? h.acceptance?.targets : section.startsWith("state.") ? h.state[section.slice(6) as keyof Handoff["state"]]
394
- : h[section as keyof Handoff];
395
- if (!Array.isArray(values) || !Number.isSafeInteger(range.total) || !Number.isSafeInteger(range.offset) ||
396
- range.offset < 0 || range.total < range.offset || range.total - range.offset !== values.length) {
397
- v.push({ code: "INVALID_HISTORY", detail: `invalid carried range for ${section}` });
398
- }
399
- }
400
- }
401
-
402
- if (h.acceptance?.criteria.some(item => item.status === "pass" && (!item.evidence || item.evidence.exit !== 0) ||
403
- item.status === "accepted" && (item.kind !== "human" || !item.accepted_event || item.target && !item.accepted_binding))) {
404
- v.push({ code: "INVALID_ACCEPTANCE", detail: "criteria need actual command or user decision evidence" });
405
- }
406
- if (h.acceptance?.targets?.some(target => target.status === "ready" && (!target.observation || !target.binding || !target.session || !target.pid || !target.entry || !target.build_event))) {
407
- v.push({ code: "INVALID_ACCEPTANCE", detail: "ready targets need original build and owned instance observations" });
408
- }
409
- if (h.loaded_skills?.some(item => !/^[a-f0-9]{64}$/.test(item.sha256) || item.sha256 !== item.artifact ||
410
- !item.path || !item.event || !Number.isSafeInteger(item.node) || item.node < 0)) {
411
- v.push({ code: "INVALID_SKILL_SOURCE", detail: "loaded skill must reference original instruction bytes and event" });
412
- }
413
- if (h.processes?.some(item => !Number.isSafeInteger(item.session) || item.session < 1 || !Number.isSafeInteger(item.node) || item.node < 0 ||
414
- !item.event || !["running", "exited"].includes(item.observed) || item.observed === "exited" && !Number.isInteger(item.exit) ||
415
- item.output && (!/^[a-f0-9]{64}$/.test(item.output.sha256) || !Number.isSafeInteger(item.output.bytes) || item.output.bytes < 0 || !item.output.path))) {
416
- v.push({ code: "INVALID_PROCESS_STATE", detail: "process state requires original observation and actual exit evidence" });
417
- }
418
- for (const c of h.state.completed) {
419
- if (c.evidence.exit !== 0) {
420
- v.push({
421
- code: "COMPLETED_WITHOUT_PASSING_EVIDENCE",
422
- detail: `"${c.what}" cites ${c.evidence.command} which exited ${c.evidence.exit}`,
423
- });
424
- }
425
- }
426
-
427
- for (const item of h.verification) {
428
- if (item.status === "pass" && item.exit !== 0) {
429
- v.push({
430
- code: "PASS_WITHOUT_ZERO_EXIT",
431
- detail: `gate "${item.gate}" is pass but exit is ${item.exit ?? "absent"}`,
432
- });
433
- }
434
- }
435
-
436
- for (const a of h.file_anchors) {
437
- if (!a.sha256) {
438
- v.push({ code: "ANCHOR_WITHOUT_SHA", detail: `anchor for ${a.path} has no sha256` });
439
- }
440
- }
441
-
442
- if (h.objective.trim().length === 0) {
443
- v.push({ code: "EMPTY_OBJECTIVE", detail: "objective is empty" });
444
- }
445
-
446
- // Every handoff is derived from some node, so a null predecessor means the
447
- // state has no provenance -- there is nothing to grep back to.
448
- if (h.predecessor === null) {
449
- v.push({ code: "PREDECESSOR_MISSING", detail: "handoff has no source node" });
450
- } else if (!h.predecessor.sha256) {
451
- // Same rule as ANCHOR_WITHOUT_SHA: a reference you cannot check is a
452
- // promise, not a pointer (D8). predecessor is the chain's most important
453
- // anchor; filling it without a digest is how the empty-string default
454
- // survived every call site. node 0 with predecessor === null is a
455
- // different case and is already covered above.
456
- v.push({
457
- code: "PREDECESSOR_WITHOUT_SHA",
458
- detail: `predecessor node ${h.predecessor.node} has no sha256`,
459
- });
460
- }
461
-
462
- // A path always contains at least the node itself.
463
- if (h.budget.nodes_on_path < 1) {
464
- v.push({
465
- code: "EMPTY_PATH",
466
- detail: `nodes_on_path is ${h.budget.nodes_on_path}, but a path includes its own node`,
467
- });
468
- }
469
-
470
- if (h.continuation_valid && !h.state.in_flight.some((i) => i.next_action.trim().length > 0) &&
471
- !(h.handoff_version === 2 && (h.history?.sections["state.in_flight"]?.offset ?? 0) > 0)) {
472
- v.push({
473
- code: "CONTINUATION_WITHOUT_NEXT_ACTION",
474
- detail: "continuation_valid is true but no in_flight item has a next_action",
475
- });
476
- }
477
-
478
- return v;
479
- }
@@ -1,139 +0,0 @@
1
- /** N.2: searchable views retain provenance; they can always be rebuilt from the log. */
2
- import type { LogEvent, ToolResultEvent } from "./events.ts";
3
- import { canonicalValue } from "./continuation.ts";
4
- import { deriveEvidenceLinks, observationResources, resourceKey, type EvidenceLink } from "./evidence-links.ts";
5
- import type { EnvironmentValidity } from "./environment.ts";
6
-
7
- export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome" | "data" | "data_ref" | "diagnostic" | "observation" | "warnings"> {
8
- node: number;
9
- event: string;
10
- kind: string;
11
- text: string;
12
- tool?: string;
13
- input?: unknown;
14
- ok?: boolean;
15
- artifact?: string;
16
- resources?: readonly string[];
17
- work_ids?: readonly string[];
18
- evidence?: readonly EvidenceLink[];
19
- source_available?: boolean;
20
- /** Runtime consumption view only; never changes the original capability receipt. */
21
- environment?: EnvironmentValidity;
22
- invocation?: { event: string; input: string; schema: string | null; parent_call_id?: string; origin?: "extension" };
23
- }
24
-
25
- /** O.15: bump when historyEntries or freshness semantics change. */
26
- export const HISTORY_INDEX_VERSION = 6;
27
- export function historyKeyInput(nodes: readonly { node: number; events: readonly LogEvent[] }[], version = HISTORY_INDEX_VERSION): string {
28
- const calls = new Map<string, string>();
29
- const self = (name: string | undefined) => name === "history" || name === "note";
30
- return canonicalValue({ version, nodes: nodes.map(part => ({ node: part.node, events: part.events.flatMap<LogEvent>(event => {
31
- // The index deliberately excludes its own reads. Effects still affect
32
- // freshness, and annotations have their own durable events (O.6).
33
- if (event.t === "tool_invocation") { calls.set(event.call_id, event.name); return self(event.name) ? [] : [event]; }
34
- if (event.t === "tool_result" && self(calls.get(event.call_id)) && event.effect === undefined) return [];
35
- if (event.t !== "assistant") return [event];
36
- for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, block.name);
37
- const blocks = event.blocks.filter(block => block.type === "text" || block.type === "tool_use" && !self(block.name));
38
- return blocks.length ? [{ ...event, blocks }] : [];
39
- }) })) });
40
- }
41
-
42
- export function historyEntries(nodes: readonly { node: number; events: readonly LogEvent[] }[]): HistoryEntry[] {
43
- const entries: HistoryEntry[] = [];
44
- const calls = new Map<string, { name: string; input: unknown }>();
45
- const invocations = new Map<string, NonNullable<HistoryEntry["invocation"]>>();
46
- for (const { node, events } of nodes) for (const e of events) {
47
- if (e.t === "assistant") {
48
- for (const block of e.blocks) if (block.type === "tool_use") calls.set(block.call_id, { name: block.name, input: block.input });
49
- const text = e.blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
50
- if (text) entries.push({ node, event: e.id, kind: e.t, text });
51
- } else if (e.t === "tool_invocation") {
52
- calls.set(e.call_id, { name: e.name, input: e.input ?? calls.get(e.call_id)?.input ?? { artifact: e.input_ref.sha256 } });
53
- invocations.set(e.call_id, { event: e.id, input: e.input_ref.sha256, schema: e.schema_ref?.sha256 ?? null,
54
- ...(e.origin ? { origin: e.origin } : {}),
55
- ...(e.parent_call_id ? { parent_call_id: e.parent_call_id } : {}) });
56
- } else if (e.t === "tool_result") {
57
- const call = calls.get(e.call_id);
58
- // Indexing query results recursively would make the index amplify itself.
59
- if (call?.name === "history" || call?.name === "note") continue;
60
- entries.push({ node, event: e.id, kind: e.t, text: e.content, ok: e.ok,
61
- ...(call ? { tool: call.name, input: call.input } : {}),
62
- ...(invocations.has(e.call_id) ? { invocation: invocations.get(e.call_id)! } : {}),
63
- resources: observationResources(e, call?.input),
64
- ...(e.effect ? { effect: e.effect } : {}), ...(e.outcome ? { outcome: e.outcome } : {}),
65
- ...(e.data !== undefined ? { data: e.data } : {}), ...(e.diagnostic ? { diagnostic: e.diagnostic } : {}),
66
- ...(e.data_ref ? { data_ref: e.data_ref } : {}),
67
- ...(e.observation ? { observation: e.observation } : {}),
68
- ...(e.warnings ? { warnings: e.warnings } : {}),
69
- ...(e.content_ref ? { artifact: e.content_ref.sha256 } : {}) });
70
- } else if (e.t === "target_observation") {
71
- entries.push({ node, event: e.id, kind: e.t, text: canonicalValue(e.observation),
72
- ...(e.observation.manifest ? { artifact: e.observation.manifest.sha256 } : {}) });
73
- } else if (e.t === "process_exit") {
74
- entries.push({ node, event: e.id, kind: e.t, text: canonicalValue(e), artifact: e.output.sha256 });
75
- } else if (["work_owner", "work_dependencies", "approval", "skill_loaded", "acceptance_contract", "acceptance_decision", "user", "next_action", "decision", "pitfall", "blocked", "question", "claim", "work_status", "evidence_link"].includes(e.t)) {
76
- entries.push({ node, event: e.id, kind: e.t, text: e.t === "user" ? e.text : canonicalValue(e),
77
- ...("work_id" in e && e.work_id ? { work_ids: [e.work_id] } : {}),
78
- ...(e.t === "evidence_link" ? { work_ids: [e.work], ...(e.resource ? { resources: [resourceKey(e.resource)] } : {}) } : {}) });
79
- }
80
- }
81
- const lookup = new Map(entries.map(entry => [entry.event, entry]));
82
- for (const link of deriveEvidenceLinks([], nodes.flatMap(node => [...node.events]))) {
83
- const source = lookup.get(link.event);
84
- const annotation = lookup.get(link.id);
85
- if (annotation) { annotation.source_available = source !== undefined; annotation.evidence = [link]; }
86
- if (source) {
87
- source.work_ids = [...new Set([...(source.work_ids ?? []), link.work])];
88
- source.evidence = [...(source.evidence ?? []), link];
89
- }
90
- }
91
- return entries;
92
- }
93
-
94
- export function historyIndex(entries: readonly HistoryEntry[]): {
95
- byEvent: ReadonlyMap<string, HistoryEntry>; byResource: ReadonlyMap<string, readonly HistoryEntry[]>; byWork: ReadonlyMap<string, readonly HistoryEntry[]>;
96
- } {
97
- const byResource = new Map<string, HistoryEntry[]>(), byWork = new Map<string, HistoryEntry[]>();
98
- const add = (index: Map<string, HistoryEntry[]>, key: string, entry: HistoryEntry): void => {
99
- const items = index.get(key);
100
- if (items) items.push(entry); else index.set(key, [entry]);
101
- };
102
- for (const entry of entries) {
103
- for (const resource of entry.resources ?? []) add(byResource, resource, entry);
104
- for (const work of entry.work_ids ?? []) add(byWork, work, entry);
105
- }
106
- return { byEvent: new Map(entries.map(entry => [entry.event, entry])), byResource, byWork };
107
- }
108
-
109
- /** Literal AND search, newest first. No regex execution, hidden ranking, or LLM. */
110
- export function queryHistory(entries: readonly HistoryEntry[], query: string, tool?: string): HistoryEntry[] {
111
- const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
112
- return entries.filter((entry) => {
113
- if (tool !== undefined && entry.tool !== tool) return false;
114
- const text = `${entry.text}\n${canonicalValue(entry.input)}`.toLowerCase();
115
- return terms.every((term) => text.includes(term));
116
- }).toReversed();
117
- }
118
-
119
- /**
120
- * Lines of an entry that contain a query term, so a caller can see the match
121
- * without reading back the whole observation. Literal and case-insensitive,
122
- * like queryHistory; an index view, never replacement evidence.
123
- */
124
- export function historyHits(text: string, terms: readonly string[], limit = 5, width = 300): { line: number; text: string }[] {
125
- const needles = terms.map((term) => term.toLowerCase()).filter(Boolean);
126
- if (needles.length === 0 || limit < 1) return [];
127
- const hits: { line: number; text: string }[] = [];
128
- const lines = text.split("\n");
129
- for (let i = 0; i < lines.length && hits.length < limit; i++) {
130
- const lower = lines[i]!.toLowerCase();
131
- if (!needles.some((needle) => lower.includes(needle))) continue;
132
- const line = lines[i]!;
133
- // Center a long line on its first match so the term survives the cut.
134
- const at = Math.min(...needles.map((needle) => lower.indexOf(needle)).filter((index) => index >= 0));
135
- const start = line.length <= width ? 0 : Math.max(0, Math.min(at - Math.floor(width / 3), line.length - width));
136
- hits.push({ line: i + 1, text: line.slice(start, start + width) });
137
- }
138
- return hits;
139
- }