@visual-z/xeno 0.2.1 → 0.3.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 (258) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +51 -13
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/commands.ts +38 -5
  8. package/src/apps/completions.ts +73 -19
  9. package/src/apps/image-draft.ts +46 -0
  10. package/src/apps/project-trust.ts +2 -42
  11. package/src/apps/provider-connect.ts +346 -0
  12. package/src/apps/render.ts +22 -47
  13. package/src/apps/resources.ts +5 -2
  14. package/src/apps/shell.ts +298 -1526
  15. package/src/apps/startup-input.ts +45 -9
  16. package/src/apps/startup.ts +128 -86
  17. package/src/apps/ui/app.ts +1383 -0
  18. package/src/apps/ui/extension-host.ts +194 -0
  19. package/src/apps/ui/image-cache.ts +78 -0
  20. package/src/apps/ui/palette.ts +88 -0
  21. package/src/apps/ui/project.ts +127 -0
  22. package/src/apps/ui/standalone.ts +146 -0
  23. package/src/apps/ui/status-model.ts +68 -0
  24. package/src/apps/ui/surface.ts +84 -0
  25. package/src/apps/ui/theme.ts +86 -0
  26. package/src/core/acceptance.ts +107 -0
  27. package/src/core/action-defaults.ts +47 -295
  28. package/src/core/benchmark.ts +2 -2
  29. package/src/core/branch-receipt.ts +24 -0
  30. package/src/core/checkpoints.ts +64 -0
  31. package/src/core/churn.ts +2 -0
  32. package/src/core/command-artifacts.ts +94 -0
  33. package/src/core/command-contract.ts +35 -0
  34. package/src/core/completion-review.ts +21 -5
  35. package/src/core/context-detail.ts +37 -0
  36. package/src/core/context-policy.ts +74 -1
  37. package/src/core/continuation.ts +17 -2
  38. package/src/core/derive.ts +29 -3
  39. package/src/core/draft-images.ts +56 -0
  40. package/src/core/environment.ts +145 -0
  41. package/src/core/evaluation-coverage.ts +74 -0
  42. package/src/core/event-validation.ts +80 -5
  43. package/src/core/events.ts +97 -3
  44. package/src/core/evidence-links.ts +7 -0
  45. package/src/core/evidence.ts +13 -3
  46. package/src/core/execution.ts +8 -2
  47. package/src/core/experiment.ts +133 -0
  48. package/src/core/export.ts +9 -0
  49. package/src/core/failure-recovery.ts +173 -0
  50. package/src/core/file-precondition.ts +72 -0
  51. package/src/core/handoff.ts +65 -4
  52. package/src/core/history-query.ts +38 -2
  53. package/src/core/history.ts +38 -1
  54. package/src/core/integration-health.ts +34 -0
  55. package/src/core/keymap.ts +8 -20
  56. package/src/core/model-picker.ts +64 -0
  57. package/src/core/native-eval.ts +25 -0
  58. package/src/core/note-guidance.ts +34 -0
  59. package/src/core/observation-view.ts +49 -8
  60. package/src/core/permissions.ts +24 -2
  61. package/src/core/process-receipt.ts +105 -0
  62. package/src/core/process-state.ts +37 -0
  63. package/src/core/project.ts +45 -18
  64. package/src/core/prompt-policy.ts +37 -0
  65. package/src/core/query-eval-contract.ts +14 -0
  66. package/src/core/read-observation.ts +82 -0
  67. package/src/core/recovery-corpus.ts +17 -0
  68. package/src/core/recovery-evaluation.ts +60 -0
  69. package/src/core/recovery-sources.ts +74 -0
  70. package/src/core/runtime-target.ts +94 -0
  71. package/src/core/search-observation.ts +97 -0
  72. package/src/core/session-tree.ts +147 -0
  73. package/src/core/settings.ts +26 -10
  74. package/src/core/skills.ts +44 -0
  75. package/src/core/sql-query.ts +211 -0
  76. package/src/core/theme.ts +36 -58
  77. package/src/core/tool-contract.ts +51 -2
  78. package/src/core/tool-discovery.ts +87 -0
  79. package/src/core/tool-failure.ts +60 -0
  80. package/src/core/tool-mode-protocol.ts +25 -0
  81. package/src/core/transcript.ts +19 -4
  82. package/src/core/trial-lifecycle.ts +14 -0
  83. package/src/core/tui-mode.ts +29 -0
  84. package/src/core/update-check.ts +97 -0
  85. package/src/core/version.ts +55 -0
  86. package/src/core/work-state.ts +49 -3
  87. package/src/core/working-set.ts +91 -0
  88. package/src/runtime/auth.ts +2 -2
  89. package/src/runtime/branch-jobs.ts +36 -0
  90. package/src/runtime/clipboard.ts +15 -4
  91. package/src/runtime/command-artifacts.ts +64 -0
  92. package/src/runtime/environment-tools.ts +60 -0
  93. package/src/runtime/evidence-notes.ts +4 -2
  94. package/src/runtime/exec-sessions.ts +206 -61
  95. package/src/runtime/execution-control.ts +1 -0
  96. package/src/runtime/extension-ui.ts +22 -30
  97. package/src/runtime/extensions.ts +95 -16
  98. package/src/runtime/harness-comparison.ts +27 -7
  99. package/src/runtime/harness-eval.ts +460 -0
  100. package/src/runtime/history-cache.ts +57 -0
  101. package/src/runtime/history-tools.ts +62 -12
  102. package/src/runtime/integrations.ts +210 -0
  103. package/src/runtime/loop.ts +1021 -268
  104. package/src/runtime/model-discovery.ts +208 -0
  105. package/src/runtime/native-eval-fixtures.ts +33 -0
  106. package/src/runtime/native-eval.ts +143 -0
  107. package/src/runtime/oauth.ts +1 -1
  108. package/src/runtime/provider-connect.ts +109 -0
  109. package/src/runtime/provider-presets.ts +87 -0
  110. package/src/runtime/query-eval-verifier.ts +28 -0
  111. package/src/runtime/recovery-coverage.ts +71 -0
  112. package/src/runtime/recovery-evidence.ts +76 -0
  113. package/src/runtime/recovery-fixtures.ts +116 -0
  114. package/src/runtime/runtime-target.ts +149 -0
  115. package/src/runtime/tool-broker.ts +137 -0
  116. package/src/runtime/tool-discovery.ts +57 -0
  117. package/src/runtime/tool-mode-eval.ts +146 -0
  118. package/src/runtime/tool-queue.ts +77 -0
  119. package/src/runtime/tools.ts +628 -183
  120. package/src/runtime/trial-control.ts +45 -0
  121. package/src/runtime/update-check.ts +47 -0
  122. package/src/runtime/window-trial.ts +2 -1
  123. package/src/runtime/working-notes.ts +33 -3
  124. package/src/runtime/workspace-lifecycle.ts +97 -0
  125. package/src/store/acceptance.ts +15 -0
  126. package/src/store/checkpoint-history.ts +43 -0
  127. package/src/store/command-artifacts.ts +96 -0
  128. package/src/store/command.ts +65 -37
  129. package/src/store/file-checkpoints.ts +104 -0
  130. package/src/store/file-index.ts +13 -1
  131. package/src/store/git-checkpoints.ts +67 -0
  132. package/src/store/observations.ts +61 -20
  133. package/src/store/process-receipts.ts +34 -0
  134. package/src/store/query-source.ts +13 -8
  135. package/src/store/recovery.ts +2 -3
  136. package/src/store/replay.ts +16 -4
  137. package/src/store/runtime-target.ts +64 -0
  138. package/src/store/search.ts +171 -132
  139. package/src/store/session-archive.ts +4 -0
  140. package/src/store/source-cancellation.ts +6 -0
  141. package/src/store/update-cache.ts +36 -0
  142. package/src/store/workspace-observation.ts +113 -0
  143. package/src/tui/ansi-spans.ts +126 -0
  144. package/src/tui/style.ts +19 -16
  145. package/src/tui/ui/atomic-draft.ts +121 -0
  146. package/src/tui/ui/canvas.ts +96 -0
  147. package/src/tui/ui/colors.ts +70 -0
  148. package/src/tui/ui/composer.ts +199 -0
  149. package/src/tui/ui/dialog.ts +379 -0
  150. package/src/tui/ui/draft.ts +23 -0
  151. package/src/tui/ui/edge.ts +186 -0
  152. package/src/tui/ui/image-size.ts +40 -0
  153. package/src/tui/ui/ink.ts +210 -0
  154. package/src/tui/ui/items.ts +40 -0
  155. package/src/tui/ui/keys.ts +40 -0
  156. package/src/tui/ui/layout.ts +74 -0
  157. package/src/tui/ui/list-model.ts +86 -0
  158. package/src/tui/ui/list-view.ts +172 -0
  159. package/src/tui/ui/mascot.ts +67 -0
  160. package/src/tui/ui/messages.ts +518 -0
  161. package/src/tui/ui/motion.ts +28 -0
  162. package/src/tui/ui/paths.ts +10 -0
  163. package/src/tui/ui/pictures.ts +78 -0
  164. package/src/tui/ui/screen.ts +421 -0
  165. package/src/tui/ui/status.ts +271 -0
  166. package/src/tui/ui/styled.ts +85 -0
  167. package/src/tui/ui/testing.ts +5 -0
  168. package/src/tui/ui/tool-card.ts +150 -0
  169. package/src/apps/extension-ui.ts +0 -330
  170. package/src/apps/resource-browser.ts +0 -75
  171. package/src/apps/session-browser.ts +0 -113
  172. package/src/apps/session-picker.ts +0 -55
  173. package/src/tui/ansi.ts +0 -67
  174. package/src/tui/autocomplete.ts +0 -29
  175. package/src/tui/chat-viewport.ts +0 -47
  176. package/src/tui/complete.ts +0 -87
  177. package/src/tui/editor.ts +0 -940
  178. package/src/tui/host.ts +0 -264
  179. package/src/tui/input.ts +0 -75
  180. package/src/tui/keys.ts +0 -378
  181. package/src/tui/markdown.ts +0 -17
  182. package/src/tui/render.ts +0 -153
  183. package/src/tui/select.ts +0 -258
  184. package/src/tui/settings.ts +0 -44
  185. package/src/tui/statusline.ts +0 -162
  186. package/src/tui/system-theme.ts +0 -700
  187. package/src/tui/terminal.ts +0 -30
  188. package/src/tui/text.ts +0 -52
  189. package/src/tui/toolview.ts +0 -386
  190. package/src/tui/transcript.ts +0 -123
  191. package/src/tui/tree.ts +0 -471
  192. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  193. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  194. package/src/vendor/pi-tui/VENDOR.md +0 -25
  195. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  196. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  197. package/src/vendor/pi-tui/colors.ts +0 -367
  198. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  199. package/src/vendor/pi-tui/components/box.ts +0 -166
  200. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  201. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  202. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  203. package/src/vendor/pi-tui/components/image.ts +0 -127
  204. package/src/vendor/pi-tui/components/input.ts +0 -494
  205. package/src/vendor/pi-tui/components/loader.ts +0 -101
  206. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  207. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  208. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  209. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  210. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  211. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  212. package/src/vendor/pi-tui/components/stack.ts +0 -154
  213. package/src/vendor/pi-tui/components/text.ts +0 -107
  214. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  215. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  216. package/src/vendor/pi-tui/editor-component.ts +0 -74
  217. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  218. package/src/vendor/pi-tui/index.ts +0 -186
  219. package/src/vendor/pi-tui/keybindings.ts +0 -320
  220. package/src/vendor/pi-tui/keys.ts +0 -1401
  221. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  222. package/src/vendor/pi-tui/latex.ts +0 -1506
  223. package/src/vendor/pi-tui/layout-node.ts +0 -51
  224. package/src/vendor/pi-tui/layout.ts +0 -449
  225. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  226. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  227. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  228. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  229. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  232. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  233. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  234. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  237. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  238. package/src/vendor/pi-tui/native/napi.h +0 -91
  239. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  240. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  241. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  244. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  245. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  246. package/src/vendor/pi-tui/native-platform.ts +0 -65
  247. package/src/vendor/pi-tui/oklab.ts +0 -233
  248. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  249. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  250. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  251. package/src/vendor/pi-tui/terminal.ts +0 -554
  252. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  253. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  254. package/src/vendor/pi-tui/tui.ts +0 -1493
  255. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  256. package/src/vendor/pi-tui/utils.ts +0 -1385
  257. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  258. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -30,6 +30,10 @@ import type { LogEvent, AssistantBlock } from "./events.ts";
30
30
  import { assertNever } from "./exhaustive.ts";
31
31
  import type { ImageContent } from "./images.ts";
32
32
  import { type Handoff, emitHandoff } from "./handoff.ts";
33
+ import { diagnosticContent } from "./tool-contract.ts";
34
+
35
+ /** O.8: persist the interpretation used to replay a frozen request. */
36
+ export const PROJECTION_VERSION = 1;
33
37
 
34
38
  export type ContentBlock =
35
39
  | ImageContent
@@ -61,15 +65,18 @@ export interface FrozenPrefix {
61
65
  }
62
66
 
63
67
  export interface ProjectInput {
68
+ /** Diagnostic only; trace never enters provider messages. */
69
+ trace?: boolean;
64
70
  frozen: FrozenPrefix;
65
71
  /** Previous segment's handoff, or null for segment 0. */
66
72
  handoff: Handoff | null;
67
73
  events: readonly LogEvent[];
68
- /** Log location given to the model so it can grep for anything the handoff omits. */
69
- logHint?: { manifestPath: string; segmentPath: string };
74
+ /** Source locations and versioned recovery guidance; workspace search does not index the log. */
75
+ logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true };
70
76
  }
71
77
 
72
78
  export interface ProjectResult {
79
+ provenance?: { version: number; messages: string[][]; omitted: string[] };
73
80
  system: string;
74
81
  tools: readonly unknown[];
75
82
  messages: Message[];
@@ -110,9 +117,11 @@ export interface ProjectViolation {
110
117
  * two things". It is a following message rather than a mutation of the user
111
118
  * turn so appending the event only appends to the projection (P7).
112
119
  */
113
- type ReplayEvent = Extract<LogEvent, { t: "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
120
+ type ReplayEvent = Extract<LogEvent, { t: "acceptance_contract" | "acceptance_decision" | "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
114
121
 
115
122
  function replayable(e: LogEvent): e is ReplayEvent {
123
+ if (e.t === "tool_result" && (e.parent_call_id !== undefined || e.origin === "extension")) return false;
124
+ if (e.t === "acceptance_contract" || e.t === "acceptance_decision") return true;
116
125
  if (e.t === "extension") return e.inContext;
117
126
  if (e.t === "user_bash") return !e.excludeFromContext;
118
127
  if (e.t === "rewind") return e.mode !== "both";
@@ -198,7 +207,7 @@ export function renderRewindMessage(e: Extract<LogEvent, { t: "rewind" }>): Mess
198
207
  */
199
208
  export function renderHandoffMessage(
200
209
  handoff: Handoff,
201
- logHint?: { manifestPath: string; segmentPath: string },
210
+ logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true },
202
211
  ): Message {
203
212
  const yaml = emitHandoff(handoff);
204
213
 
@@ -221,8 +230,10 @@ export function renderHandoffMessage(
221
230
  "detail this state does not carry, read it rather than guessing:",
222
231
  ` - segment index: ${logHint.manifestPath}`,
223
232
  ` - previous segment: ${logHint.segmentPath}`,
224
- "Each line is one JSON event. Search it with history; read a line range with read,",
225
- "or filter with bash and jq. The grep tool skips the log directory.",
233
+ ...(logHint.scopedGuidance !== true ? [
234
+ "Each line is one JSON event. Search it with history; read a line range with read,",
235
+ "or filter with bash and jq. The grep tool skips the log directory.",
236
+ ] : ["Each line is one JSON event. Recover originals using the declared tools' contracts; if evidence is inaccessible, state the gap instead of guessing."]),
226
237
  );
227
238
  }
228
239
 
@@ -251,9 +262,11 @@ export function renderHandoffMessage(
251
262
  export function projectContext(input: ProjectInput): ProjectResult {
252
263
  const violations: ProjectViolation[] = [];
253
264
  const messages: Message[] = [];
265
+ const sources: string[][] = [];
266
+ const append = (message: Message, ids: string[]) => { messages.push(message); sources.push(ids); };
254
267
 
255
268
  if (input.handoff) {
256
- messages.push(renderHandoffMessage(input.handoff, input.logHint));
269
+ append(renderHandoffMessage(input.handoff, input.logHint), [`handoff:${input.handoff.predecessor?.node ?? "root"}`]);
257
270
  }
258
271
 
259
272
  const events = input.events.filter(replayable);
@@ -262,10 +275,12 @@ export function projectContext(input: ProjectInput): ProjectResult {
262
275
  // a SINGLE user message. Splitting them across messages teaches the model to stop
263
276
  // issuing parallel calls, which is a silent, permanent capability regression.
264
277
  let pending: ContentBlock[] = [];
278
+ let pendingSources: string[] = [];
265
279
  const flushPending = () => {
266
280
  if (pending.length > 0) {
267
- messages.push({ role: "user", content: pending });
281
+ append({ role: "user", content: pending }, pendingSources);
268
282
  pending = [];
283
+ pendingSources = [];
269
284
  }
270
285
  };
271
286
 
@@ -273,29 +288,38 @@ export function projectContext(input: ProjectInput): ProjectResult {
273
288
  let awaiting = new Set<string>();
274
289
 
275
290
  for (const e of events) {
291
+ if (e.t === "acceptance_contract" || e.t === "acceptance_decision") {
292
+ flushPending();
293
+ if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `acceptance event ${e.id} follows unanswered tools` });
294
+ append({ role: "user", content: [{ type: "text", text: e.t === "acceptance_contract"
295
+ ? `Declared acceptance contract (execution success alone does not satisfy it): ${JSON.stringify(e.contract)}`
296
+ : `User accepted criterion ${e.criterion} of contract ${e.contract_id} for request ${e.request}.` +
297
+ (e.target_binding ? ` Bound to runtime instance ${e.target_binding}, observation ${e.target_observation}. Later runtime changes invalidate this acceptance.` : "") }] }, [e.id]);
298
+ continue;
299
+ }
276
300
  if (e.t === "extension") {
277
301
  flushPending();
278
302
  if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `custom message ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}` });
279
- messages.push({ role: "user", content: [{ type: "text", text: e.content }] });
303
+ append({ role: "user", content: [{ type: "text", text: e.content }] }, [e.id]);
280
304
  continue;
281
305
  }
282
306
  if (e.t === "user_bash") {
283
307
  flushPending();
284
- messages.push({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] });
308
+ append({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] }, [e.id]);
285
309
  continue;
286
310
  }
287
311
  if (e.t === "snapshot") {
288
312
  flushPending();
289
- messages.push({ role: "user", content: [{ type: "text",
313
+ append({ role: "user", content: [{ type: "text",
290
314
  text: `Workspace observation: changes outside declared file edits: ${e.undeclared.join(", ")}. ${REREAD_INSTRUCTION}`,
291
- }] });
315
+ }] }, [e.id]);
292
316
  continue;
293
317
  }
294
318
  if (e.t === "rewind") {
295
319
  // Flushed first for the same reason a user turn is: it opens a user
296
320
  // message, and a pending tool_result batch has to close before it.
297
321
  flushPending();
298
- messages.push(renderRewindMessage(e));
322
+ append(renderRewindMessage(e), [e.id]);
299
323
  continue;
300
324
  }
301
325
  if (e.t === "user" || e.t === "guidance") {
@@ -307,8 +331,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
307
331
  });
308
332
  awaiting = new Set();
309
333
  }
310
- messages.push({ role: "user", content: [...(e.text || e.t === "guidance" || !e.images?.length ? [{ type: "text" as const, text: e.text }] : []),
311
- ...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] });
334
+ append({ role: "user", content: [...(e.text || e.t === "guidance" || !e.images?.length ? [{ type: "text" as const, text: e.text }] : []),
335
+ ...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] }, [e.id]);
312
336
  continue;
313
337
  }
314
338
  if (e.t === "interrupt") {
@@ -316,7 +340,7 @@ export function projectContext(input: ProjectInput): ProjectResult {
316
340
  // look-ahead mutation of that turn would rewrite it once a later
317
341
  // event (tool_use, another user) was already projected after it.
318
342
  flushPending();
319
- messages.push({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] });
343
+ append({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] }, [e.id]);
320
344
  continue;
321
345
  }
322
346
 
@@ -328,7 +352,7 @@ export function projectContext(input: ProjectInput): ProjectResult {
328
352
  detail: `assistant turn ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}`,
329
353
  });
330
354
  }
331
- messages.push({ role: "assistant", content: e.blocks.map(toContentBlock) });
355
+ append({ role: "assistant", content: e.blocks.map(toContentBlock) }, [e.id]);
332
356
  awaiting = new Set(
333
357
  e.blocks.filter((b) => b.type === "tool_use").map((b) => (b as { call_id: string }).call_id),
334
358
  );
@@ -344,10 +368,11 @@ export function projectContext(input: ProjectInput): ProjectResult {
344
368
  continue;
345
369
  }
346
370
  awaiting.delete(e.call_id);
371
+ pendingSources.push(e.id);
347
372
  pending.push({
348
373
  type: "tool_result",
349
374
  tool_use_id: e.call_id,
350
- content: e.content,
375
+ content: diagnosticContent(e.content, e.diagnostic) + (e.warnings?.length ? "\nObserver warnings (operation result retained): " + JSON.stringify(e.warnings) : ""),
351
376
  ...(e.ok ? {} : { is_error: true }),
352
377
  });
353
378
  // Images follow the paired text result, so APIs without image tool outputs receive a user image.
@@ -368,6 +393,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
368
393
  }
369
394
 
370
395
  return {
396
+ ...(input.trace ? { provenance: { version: PROJECTION_VERSION, messages: sources,
397
+ omitted: input.events.filter(e => !sources.some(ids => ids.includes(e.id))).map(e => e.id) } } : {}),
371
398
  system: input.frozen.system,
372
399
  tools: input.frozen.tools,
373
400
  messages,
@@ -0,0 +1,37 @@
1
+ /** R10 / D6: stable scope and completion rules; execution details belong to tool contracts. */
2
+ export function taskGuidance(): string {
3
+ return [
4
+ "How to work:",
5
+ "- Keep going until the request is fully resolved. The user's request defines scope; a tool profile only selects capabilities. State reasonable assumptions; ask only for decisions the user must make.",
6
+ "- Implementation: understand the relevant code, reproduce bugs, finish every requested change and verify the final state with the project's tests, build or type check. Finish with what changed and the command that verified it, or the specific blocker. A research answer does not complete an implementation request.",
7
+ "- Research: answer the requested questions with original sources, distinguish evidence from inference, and report gaps. Do not change files merely to establish completion.",
8
+ "- Data analysis: calculate over the complete declared inputs; report sources, denominators, exclusions and uncertainty. Truncated previews cannot establish totals.",
9
+ "- Runtime or visual work: bind checks to the actual build and running target. Automated checks do not establish human acceptance; report any required acceptance as pending.",
10
+ "- Put independent reads and searches in one response; they run in parallel. After a command fails, read the end of its output first. If the same approach fails twice, change the approach instead of retrying it.",
11
+ "- Use only declared capabilities and obey admission decisions. If required evidence is inaccessible, state the specific blocker without inventing success, bypassing permission or replaying an operation with uncertain effects.",
12
+ ].join("\n");
13
+ }
14
+
15
+ export interface ToolGuidance {
16
+ /** Only builtin companions with these exact contracts can satisfy the dependency. */
17
+ requires: readonly string[];
18
+ text: string;
19
+ }
20
+
21
+ /** Capability projection is deterministic; excluding a companion cannot add advice. */
22
+ export function toolDescription(description: string, guidance: readonly ToolGuidance[], tools: readonly string[]): string {
23
+ return [description, ...guidance.filter(item => item.requires.every(name => tools.includes(name))).map(item => item.text)].join(" ");
24
+ }
25
+
26
+ export function recoveryInstructions(tools: readonly string[]): string {
27
+ return (tools.includes("history")
28
+ ? "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; event=ID reads one original. history(work_id=ID) recovers work. "
29
+ : "Original user messages and evidence remain at the source references. The original-source recovery capability is unavailable; state any resulting evidence gap instead of guessing. ") +
30
+ "Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting.";
31
+ }
32
+
33
+ /** Preserve the source locator even when the interface for retrieving it is unavailable. */
34
+ export function artifactRecovery(sha256: string, offset: number, tools: readonly string[] = ["history"]): string {
35
+ const source = `artifact=${sha256} offset=${offset}`;
36
+ return tools.includes("history") ? `history ${source}` : `${source}; recovery capability unavailable`;
37
+ }
@@ -0,0 +1,14 @@
1
+ /** Q.21: task-specific shape advice cannot reveal the independent numeric oracle. */
2
+ import type { ToolModeTask } from "./tool-mode-protocol.ts";
3
+
4
+ export function queryEvalContract(task: ToolModeTask, paths: readonly string[], answerSha256: string) {
5
+ const columns = task === "tool-small" ? ["amount"] : task === "tool-batch" ? ["total"] : ["team", "total"];
6
+ const rowCount = task === "tool-aggregate" ? 4 : 1;
7
+ const operation = task === "tool-small" ? "select amount; return one row with only amount" : task === "tool-batch"
8
+ ? "sum amount across all records as total; return one row with only total per source"
9
+ : "keep enabled=true, group by team, sum amount as total, and sort by team ascending; return four rows with columns team,total in that order";
10
+ const instruction = "Use query on each of " + paths.join(", ") + " to " + operation +
11
+ '. Write compact JSON with a trailing newline to answer.txt: [{"path":"data-0.jsonl","rows":[...]}], one item per source in the listed order. ' +
12
+ "Preserve all fixture inputs and run bun run gate.";
13
+ return { instruction, columns, rowCount, paths: [...paths], answerSha256 };
14
+ }
@@ -0,0 +1,82 @@
1
+ /** R2 / D8: source bytes, an unclipped observation, and the visible projection have distinct identities. */
2
+ import type { ArtifactRef } from "./events.ts";
3
+ import { artifactRecovery } from "./prompt-policy.ts";
4
+ import { headTailSelection, isObservationArtifact as ref, isBytePreview, type BytePreview } from "./observation-view.ts";
5
+
6
+ export interface ObservationReceipt {
7
+ version: 1;
8
+ source: { host: string; workspace: string; resource: string; sha256: string; raw: ArtifactRef };
9
+ artifact: ArtifactRef;
10
+ /** R4: content-addressed version premise, separate from this formatted observation. */
11
+ descriptor?: ArtifactRef;
12
+ selection: { start: number; end: number; total_lines: number };
13
+ preview: BytePreview;
14
+ }
15
+
16
+ const encoder = new TextEncoder();
17
+ const size = (text: string) => encoder.encode(text).length;
18
+ /** Existing assumed preview cap; archival removes data loss without changing the display policy. */
19
+ const READ_MAX_LINE_CHARS = 2000;
20
+
21
+ export function readObservationText(header: string, lines: readonly string[], start: number): string {
22
+ return header + lines.map((line, i) => `${start + i}\t${line}`).join("\n");
23
+ }
24
+
25
+ /** Map two successive cuts (per line and total bytes) back to the same immutable observation. */
26
+ export function previewReadObservation(header: string, lines: readonly string[], start: number, artifact: string,
27
+ maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): { text: string; preview: ObservationReceipt["preview"]; semantic: string } {
28
+ const spans: { view: number; source: number; bytes: number }[] = [];
29
+ let text = "", source = 0, view = 0;
30
+ const mapped = (value: string, offset: number) => {
31
+ const bytes = size(value);
32
+ if (bytes) spans.push({ view, source: offset, bytes });
33
+ text += value; view += bytes;
34
+ };
35
+ const synthetic = (value: string) => { text += value; view += size(value); };
36
+ mapped(header, 0); source += size(header);
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i]!, prefix = `${start + i}\t`;
39
+ let end = Math.min(line.length, maxLineChars);
40
+ // JavaScript string indices are UTF-16; a character can occupy a surrogate pair.
41
+ if (end > 0 && end < line.length && /[\uD800-\uDBFF]/.test(line[end - 1]!)) end--;
42
+ const kept = prefix + line.slice(0, end);
43
+ mapped(kept, source);
44
+ if (end < line.length) synthetic(` [line truncated: ${line.length} chars; ${artifactRecovery(artifact, source + size(kept), tools)}]`);
45
+ source += size(prefix + line);
46
+ if (i < lines.length - 1) { mapped("\n", source); source++; }
47
+ }
48
+ const bytes = encoder.encode(text);
49
+ const selected = headTailSelection(bytes, bytes, bytes.length, maxBytes,
50
+ omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
51
+ const visible: [number, number][] = [];
52
+ for (const [lo, hi] of selected.visible) for (const span of spans) {
53
+ const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
54
+ if (end <= begin) continue;
55
+ const a = span.source + begin - span.view, b = span.source + end - span.view;
56
+ const last = visible.at(-1);
57
+ if (last?.[1] === a) last[1] = b;
58
+ else visible.push([a, b]);
59
+ }
60
+ let next = 0;
61
+ for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
62
+ const full = encoder.encode(readObservationText(header, lines, start));
63
+ const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(Math.max(lo, size(header)), Math.max(hi, size(header))))).join("");
64
+ return { text: selected.text, semantic, preview: { unit: "utf8_bytes", complete: next === source,
65
+ visible, next_offset: next === source ? null : next } };
66
+ }
67
+
68
+ const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
69
+ const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
70
+ const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
71
+
72
+ /** Optional on old logs, strict when present. A cursor cannot certify bytes outside its artifact. */
73
+ export function isObservationReceipt(value: unknown): value is ObservationReceipt {
74
+ if (!record(value) || value["version"] !== 1 || !ref(value["artifact"])) return false;
75
+ if (value["descriptor"] !== undefined && !ref(value["descriptor"])) return false;
76
+ const source = value["source"], selection = value["selection"], preview = value["preview"];
77
+ if (!record(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") ||
78
+ !digest(source["sha256"]) || !ref(source["raw"]) || source["sha256"] !== source["raw"].sha256) return false;
79
+ if (!record(selection) || !natural(selection["start"]) || selection["start"] < 1 || !natural(selection["end"]) ||
80
+ !natural(selection["total_lines"]) || selection["end"] < selection["start"] || selection["end"] > selection["total_lines"]) return false;
81
+ return isBytePreview(preview, value["artifact"].bytes);
82
+ }
@@ -0,0 +1,17 @@
1
+ /** R12c: curated mechanisms, never copied private transcript contents or population estimates. */
2
+ export const RECOVERY_SOURCE_REPORT = { path: "docs/research/local-agent-tool-audit-2026-10-06.md",
3
+ sha256: "a4c83ec9e5c52ea6500434673f0e90c171e55aa247d293dc3f09c893ec604832" } as const;
4
+ export const RECOVERY_CASES = [
5
+ {id:"recovery-missing-path",family:"parameters",partition:"development",source:"E2",basis:"observed",scenario:"An edit omits its required file identity."},
6
+ {id:"recovery-stale-version",family:"version",partition:"development",source:"section-2.2-replacement-mismatch",basis:"derived-scenario",scenario:"An obsolete file digest must not authorize an edit over newer content; the source mismatch alone did not establish a stale-version cause."},
7
+ {id:"recovery-long-output",family:"output",partition:"development",source:"X2",basis:"observed",scenario:"A clipped long line hides the needed value; recover immutable original bytes."},
8
+ {id:"recovery-api-mismatch",family:"environment",partition:"holdout",source:"E4",basis:"portable-analogue",scenario:"An older local API rejects a named option; inspect its actual contract before retrying."},
9
+ {id:"recovery-closed-process",family:"lifecycle",partition:"development",source:"E5",basis:"derived-scenario",scenario:"A finished process cannot accept more input; the original unknown ID did not establish why ownership was lost."},
10
+ {id:"recovery-unchanged-retry",family:"recovery",partition:"development",source:"E3",basis:"derived-scenario",scenario:"Changing replacement wording does not repair an unchanged missing anchor; this extends the source's identical retry mechanism."},
11
+ {id:"recovery-runtime-acceptance",family:"acceptance",partition:"holdout",source:"E6",basis:"scenario-from-user-question",scenario:"A build is not a running current instance or author acceptance; bind the preview to its build."},
12
+ {id:"recovery-eof",family:"parameters",partition:"holdout",source:"section-2.2-read-range",basis:"observed",scenario:"A line request exceeds the real file boundary; recover from the reported range."},
13
+ ] as const;
14
+ export type RecoveryTask = typeof RECOVERY_CASES[number]["id"];
15
+ export const RECOVERY_TASKS = RECOVERY_CASES.map(item=>item.id);
16
+ export const recoveryCase = (task:string) => RECOVERY_CASES.find(item=>item.id===task);
17
+ export type RecoveryPartition = "all" | "development" | "holdout";
@@ -0,0 +1,60 @@
1
+ /** R12c: metrics are projections of original calls, not generated tool-result prose. */
2
+ import type {LogEvent} from "./events.ts";
3
+ import {advanceRecovery,emptyRecoveryState} from "./failure-recovery.ts";
4
+ import {resolveEvidence} from "./evidence.ts";
5
+
6
+ export interface RecoveryMetrics {
7
+ toolCalls:number; recordedInvocations:number; outerCalls:number; nestedCalls:number; unresolvedCalls:number; toolFailures:number;
8
+ firstToolSuccess:boolean|null; firstPassTaskSuccess:boolean|null; finalTaskSuccess:boolean|null;
9
+ recoveryCalls:number; unchangedFailureRetries:number; classifiedFailures:number; unclassifiedFailures:number;
10
+ originalEvidenceRetrievals:number; unsupportedClaims:number;
11
+ }
12
+ export function deriveRecoveryMetrics(nodes:readonly {node:number;events:readonly LogEvent[]}[],verifiedRetrievals:ReadonlySet<string>,
13
+ finalTaskSuccess:boolean|null,identify:(path:string)=>string):RecoveryMetrics{
14
+ type Call={name:string;ordinal:number;recorded:boolean;nested:boolean;ok:boolean|null};
15
+ const calls=new Map<string,Call>();let ordinal=0,firstFailure=Infinity,repeated=0,classified=0,unclassified=0,retrieved=0,unsupported=0;
16
+ let state=emptyRecoveryState();
17
+ for(const part of nodes)for(const [index,event] of part.events.entries()){
18
+ ordinal++;
19
+ if(event.t==="assistant")for(const block of event.blocks)if(block.type==="tool_use"){
20
+ const key=`${part.node}:${block.call_id}`;
21
+ if(calls.has(key))throw Error("duplicate recovery call identity");
22
+ calls.set(key,{name:block.name,ordinal,recorded:false,nested:false,ok:null});
23
+ }
24
+ if(event.t==="tool_invocation"){
25
+ const key=`${part.node}:${event.call_id}`,prior=calls.get(key);
26
+ if(prior?.recorded)throw Error("duplicate recovery invocation");
27
+ calls.set(key,{name:event.name,ordinal:prior?.ordinal??ordinal,recorded:true,nested:event.parent_call_id!==undefined,ok:prior?.ok??null});
28
+ }
29
+ if(event.t==="tool_result"){
30
+ const key=`${event.invocation_ref?.node??part.node}:${event.call_id}`,call=calls.get(key);
31
+ if(!call||call.ok!==null)throw Error("unpaired recovery result");
32
+ call.ok=event.ok;
33
+ if(!event.ok){firstFailure=Math.min(firstFailure,ordinal);if(event.diagnostic)classified++;else unclassified++;}
34
+ if(call.name==="history"&&event.ok&&verifiedRetrievals.has(`${part.node}:${event.id}`))retrieved++;
35
+ }
36
+ if(event.t==="claim"&&!resolveEvidence(part.events.slice(0,index),event.evidence).ok)unsupported++;
37
+ state=advanceRecovery(state,[event],identify);
38
+ if(event.t==="tool_result"&&!event.ok&&[...state.failures.values()].some(run=>run.count>1&&run.sources.at(-1)===event.id))repeated++;
39
+ }
40
+ const rows=[...calls.values()],failed=rows.filter(call=>call.ok===false).length,unresolved=rows.filter(call=>call.ok===null).length;
41
+ // An invocation record proves the attempt was logged, not that schema or
42
+ // permission checks admitted its effect. Rejected requests have receipts too.
43
+ return {toolCalls:rows.length,recordedInvocations:rows.filter(call=>call.recorded).length,outerCalls:rows.filter(call=>!call.nested).length,
44
+ nestedCalls:rows.filter(call=>call.nested).length,unresolvedCalls:unresolved,toolFailures:failed,firstToolSuccess:rows[0]?.ok??null,
45
+ firstPassTaskSuccess:finalTaskSuccess===null?null:finalTaskSuccess&&failed===0&&unresolved===0,
46
+ finalTaskSuccess,recoveryCalls:rows.filter(call=>call.ordinal>firstFailure).length,unchangedFailureRetries:repeated,
47
+ classifiedFailures:classified,unclassifiedFailures:unclassified,originalEvidenceRetrievals:retrieved,unsupportedClaims:unsupported};
48
+ }
49
+ export function isRecoveryMetrics(value:unknown):value is RecoveryMetrics{
50
+ if(!value||typeof value!=="object")return false;
51
+ const v=value as Record<string,unknown>;
52
+ const numbers=["toolCalls","recordedInvocations","outerCalls","nestedCalls","unresolvedCalls","toolFailures","recoveryCalls","unchangedFailureRetries","classifiedFailures","unclassifiedFailures","originalEvidenceRetrievals","unsupportedClaims"];
53
+ if(numbers.some(key=>!Number.isSafeInteger(v[key])||Number(v[key])<0))return false;
54
+ if(["firstToolSuccess","firstPassTaskSuccess","finalTaskSuccess"].some(key=>v[key]!==null&&typeof v[key]!=="boolean"))return false;
55
+ const n=(key:string)=>Number(v[key]);
56
+ return n("outerCalls")+n("nestedCalls")===n("toolCalls")&&n("recordedInvocations")<=n("toolCalls")
57
+ &&n("unresolvedCalls")+n("toolFailures")<=n("toolCalls")&&n("classifiedFailures")+n("unclassifiedFailures")===n("toolFailures")
58
+ &&n("recoveryCalls")<=n("toolCalls")&&n("unchangedFailureRetries")<=n("toolFailures")&&n("originalEvidenceRetrievals")<=n("toolCalls")-n("toolFailures")-n("unresolvedCalls")
59
+ &&(v["firstPassTaskSuccess"]!==true||v["finalTaskSuccess"]===true&&n("toolFailures")===0&&n("unresolvedCalls")===0);
60
+ }
@@ -0,0 +1,74 @@
1
+ /** R9 / D8: navigation into original ancestry, never a semantic summary or permission grant. */
2
+ import type { LogEvent } from "./events.ts";
3
+ import { historyEntries, type HistoryEntry } from "./history-query.ts";
4
+ import { isEnvironmentReceipt } from "./environment.ts";
5
+ import { resourceKey } from "./evidence-links.ts";
6
+ import type { ToolDiagnostic } from "./tool-contract.ts";
7
+
8
+ export const RECOVERY_SECTIONS = ["users", "failures", "environment", "next_actions"] as const;
9
+ export type RecoverySection = typeof RECOVERY_SECTIONS[number];
10
+ export const RECOVERY_KINDS = { users: "user", failures: "failure", environment: "environment", next_actions: "next_action" } as const;
11
+ export type RecoveryNodes = readonly { node: number; events: readonly LogEvent[] }[];
12
+ export type RecoveryReference = {
13
+ node: number; event: string; kind: "user" | "failure" | "environment" | "next_action";
14
+ /** This is the recorded role, not proof of current authorization or human authorship. */
15
+ provenance: "user_message" | "observation" | "model_annotation";
16
+ abandoned?: true; tool?: string; work?: string;
17
+ resources?: readonly string[];
18
+ /** A failed request can name a resource without having observed it. */
19
+ requested_resource?: string;
20
+ diagnostic?: Pick<ToolDiagnostic, "code" | "execution" | "effects">;
21
+ host?: string; workspace?: string; cwd?: string; program?: string;
22
+ };
23
+ export type RecoverySources = Record<RecoverySection, RecoveryReference[]>;
24
+ export type RecoveryRanges = Record<RecoverySection, { total: number; omitted: number }>;
25
+
26
+ /** Newest first within each role; no keywords decide which user sentence is a constraint. */
27
+ export function recoverySources(nodes: RecoveryNodes, entries: readonly HistoryEntry[] = historyEntries(nodes)): RecoverySources {
28
+ const abandoned = new Set(nodes.flatMap(part => part.events.flatMap(event => event.t === "interrupt" ? [event.user_id] : [])));
29
+ const result: RecoverySources = { users: [], failures: [], environment: [], next_actions: [] };
30
+ for (const entry of entries.toReversed()) {
31
+ const source = { node: entry.node, event: entry.event };
32
+ if (entry.kind === "user") result.users.push({ ...source, kind: "user", provenance: "user_message",
33
+ ...(abandoned.has(entry.event) ? { abandoned: true } : {}) });
34
+ if (entry.kind === "next_action") result.next_actions.push({ ...source, kind: "next_action", provenance: "model_annotation",
35
+ ...(entry.work_ids?.[0] ? { work: entry.work_ids[0] } : {}) });
36
+ if (entry.kind !== "tool_result") continue;
37
+ if (entry.ok === false) {
38
+ const input = entry.input !== null && typeof entry.input === "object" ? entry.input as Record<string, unknown> : {};
39
+ const requested = typeof input["path"] === "string" ? input["path"] : typeof input["cwd"] === "string" ? input["cwd"] : undefined;
40
+ const diagnostic = entry.diagnostic;
41
+ result.failures.push({ ...source, kind: "failure", provenance: "observation", ...(entry.tool ? { tool: entry.tool } : {}),
42
+ ...(entry.resources?.length ? { resources: entry.resources } : {}),
43
+ ...(requested ? { requested_resource: resourceKey(requested) } : {}),
44
+ ...(diagnostic ? { diagnostic: { code: diagnostic.code,
45
+ ...(diagnostic.execution ? { execution: diagnostic.execution } : {}), ...(diagnostic.effects ? { effects: diagnostic.effects } : {}) } } : {}) });
46
+ }
47
+ if (entry.tool === "environment") {
48
+ const receipt = entry.data;
49
+ // Only a validated actual receipt contributes identity. Text that looks
50
+ // like a host or a successful probe does not become a machine fact (D11).
51
+ const identity = isEnvironmentReceipt(receipt) && entry.effect?.kind === "command" && entry.effect.exit === receipt.evidence.exit
52
+ ? { host: receipt.source.host, workspace: receipt.source.workspace, cwd: receipt.source.cwd, program: receipt.query.program } : {};
53
+ result.environment.push({ ...source, kind: "environment", provenance: "observation", ...identity });
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /** Source labels are stable even when the runtime attaches a fresh validity view. */
60
+ export function recoveryPage(sources: RecoverySources, entries: readonly HistoryEntry[], section: RecoverySection, offset: number, limit: number): {
61
+ total: number; offset: number; next: number | null; items: { source: RecoveryReference; original: HistoryEntry }[];
62
+ } {
63
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
64
+ throw new RangeError("recovery offset must be nonnegative and limit must be 1..100");
65
+ }
66
+ const indexed = new Map(entries.map(entry => [entry.event, entry]));
67
+ const references = sources[section], end = Math.min(references.length, offset + limit);
68
+ return { total: references.length, offset, next: end < references.length ? end : null,
69
+ items: references.slice(offset, end).map(source => {
70
+ const original = indexed.get(source.event);
71
+ if (!original || original.node !== source.node) throw new Error("recovery source is absent from the selected ancestry");
72
+ return { source, original };
73
+ }) };
74
+ }
@@ -0,0 +1,94 @@
1
+ /** R8: declarations select a target; only an owned runtime observation supplies its identity. */
2
+ import { parseCommandArtifactPlan, validArtifactPath, type CommandArtifactPlan } from "./command-artifacts.ts";
3
+ import type { ArtifactRef } from "./events.ts";
4
+ import { isObservationArtifact } from "./observation-view.ts";
5
+ import type { ProcessSource } from "./process-receipt.ts";
6
+
7
+ export type RuntimeTarget = { id: string; build: string; artifacts: CommandArtifactPlan; entry: string;
8
+ interpreter?: string; args?: string[]; workdir?: string; port?: number };
9
+ export type TargetBuild = { node: number; event: string; manifest: ArtifactRef };
10
+ export type RuntimeInstance = { id: string; session: number; pid: number; source: ProcessSource;
11
+ entry: { path: string; sha256: string }; program: { path: string; sha256: string; device: string; inode: string } };
12
+ export type RuntimeTargetReceipt = {
13
+ kind: "runtime_target"; version: 1; target: string; request: string; contract_event: string;
14
+ state: "ready" | "not_ready" | "stale" | "unknown"; reason: string; observed_at: string;
15
+ source: ProcessSource; build?: TargetBuild; instance?: RuntimeInstance; port?: number;
16
+ checks: { build: boolean; inputs: boolean; artifacts: boolean; process: boolean; image: boolean; port: boolean | null };
17
+ manifest?: ArtifactRef;
18
+ };
19
+ export type TargetAcceptance = { id: string; build: string; status: "not_run" | RuntimeTargetReceipt["state"];
20
+ observation?: string; binding?: string; session?: number; pid?: number; entry?: string; port?: number; build_event?: string; reason?: string };
21
+
22
+ const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
23
+ const id = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(value);
24
+ const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
25
+ const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
26
+ const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
27
+ const source = (value: unknown): value is ProcessSource => record(value) && ["host", "workspace", "cwd", "runtime"].every(key => text(value[key]));
28
+
29
+ export function parseRuntimeTarget(value: unknown): RuntimeTarget {
30
+ if (!record(value) || Object.keys(value).some(key => !["id", "build", "artifacts", "entry", "interpreter", "args", "workdir", "port"].includes(key)) ||
31
+ !id(value["id"]) || !id(value["build"]) || !validArtifactPath(value["entry"])) throw Error("invalid runtime target declaration");
32
+ const artifacts = parseCommandArtifactPlan(value["artifacts"]);
33
+ if (!artifacts.paths.includes(value["entry"])) throw Error("target entry must be an observed build artifact");
34
+ const interpreter = value["interpreter"], args = value["args"], workdir = value["workdir"], port = value["port"];
35
+ if (interpreter !== undefined && (typeof interpreter !== "string" || !interpreter.startsWith("/") || /[\x00-\x1f\x7f]/.test(interpreter) || interpreter.length > 4096)) throw Error("target interpreter needs an explicit absolute path");
36
+ // Assumed argv budget prevents declarations from becoming an unbounded prompt or exec payload.
37
+ if (args !== undefined && (!Array.isArray(args) || args.length > 64 || !args.every(arg => typeof arg === "string" && !arg.includes("\0")) || JSON.stringify(args).length > 8192)) throw Error("target args must be a bounded literal string array");
38
+ if (workdir !== undefined && workdir !== "." && !validArtifactPath(workdir)) throw Error("target workdir must stay inside the workspace");
39
+ if (port !== undefined && (!natural(port) || port < 1 || port > 65535)) throw Error("target port must be 1..65535 on IPv4 loopback");
40
+ return { id: value["id"], build: value["build"], entry: value["entry"], artifacts,
41
+ ...(interpreter !== undefined ? { interpreter: interpreter as string } : {}), ...(args !== undefined ? { args: [...args as string[]] } : {}),
42
+ ...(workdir !== undefined ? { workdir: workdir as string } : {}), ...(port !== undefined ? { port: port as number } : {}) };
43
+ }
44
+
45
+ export function targetIsReady(receipt: Pick<RuntimeTargetReceipt, "build" | "instance" | "checks">): boolean {
46
+ return receipt.build !== undefined && receipt.instance !== undefined && receipt.checks.build && receipt.checks.inputs && receipt.checks.artifacts &&
47
+ receipt.checks.process && receipt.checks.image && receipt.checks.port !== false;
48
+ }
49
+ export function isRuntimeTargetReceipt(value: unknown): value is RuntimeTargetReceipt {
50
+ if (!record(value) || value["kind"] !== "runtime_target" || value["version"] !== 1 || !id(value["target"]) || !text(value["request"]) ||
51
+ !text(value["contract_event"]) || !text(value["observed_at"]) || !text(value["reason"]) || !source(value["source"]) ||
52
+ !["ready", "not_ready", "stale", "unknown"].includes(String(value["state"]))) return false;
53
+ const build = value["build"], instance = value["instance"], checks = value["checks"], port = value["port"];
54
+ if (build !== undefined && (!record(build) || !natural(build["node"]) || !text(build["event"]) || !isObservationArtifact(build["manifest"]))) return false;
55
+ if (instance !== undefined && (!record(instance) || !text(instance["id"]) || !natural(instance["session"]) || instance["session"] < 1 ||
56
+ !natural(instance["pid"]) || instance["pid"] < 1 || !source(instance["source"]) ||
57
+ !["host", "workspace", "cwd", "runtime"].every(key => (instance["source"] as Record<string, string>)[key] === (value["source"] as Record<string, string>)[key]) ||
58
+ !record(instance["entry"]) || !text(instance["entry"]["path"]) || !digest(instance["entry"]["sha256"]) ||
59
+ !record(instance["program"]) || !text(instance["program"]["path"]) || !digest(instance["program"]["sha256"]) ||
60
+ !text(instance["program"]["device"]) || !text(instance["program"]["inode"]))) return false;
61
+ if (!record(checks) || !["build", "inputs", "artifacts", "process", "image"].every(key => typeof checks[key] === "boolean") ||
62
+ !(checks["port"] === null || typeof checks["port"] === "boolean") ||
63
+ (port === undefined ? checks["port"] !== null : !natural(port) || port < 1 || port > 65535 || checks["port"] === null)) return false;
64
+ if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
65
+ return value["state"] !== "ready" || targetIsReady(value as RuntimeTargetReceipt) && isObservationArtifact(value["manifest"]);
66
+ }
67
+
68
+ /** lsof -F0 is a field protocol; matching a process name or display substring proves nothing. */
69
+ export function lsofFiles(output: string, pid: number): Record<string, string>[] {
70
+ let process = -1, file: Record<string, string> | undefined;
71
+ const files: Record<string, string>[] = [];
72
+ for (const chunk of output.split("\0")) {
73
+ const field = chunk.replace(/^\n/, "");
74
+ if (!field) continue;
75
+ const key = field[0]!, value = field.slice(1);
76
+ if (key === "p") { process = Number(value); file = undefined; }
77
+ else if (key === "f") { file = { f: value }; if (process === pid) files.push(file); }
78
+ else if (file && process === pid) {
79
+ if (key === "T") { const split = value.indexOf("="); if (split > 0) file[`T${value.slice(0, split)}`] = value.slice(split + 1); }
80
+ else file[key] = value;
81
+ }
82
+ }
83
+ return files;
84
+ }
85
+ export function lsofOwnsImage(output: string, pid: number, device: string, inode: string): boolean {
86
+ return lsofFiles(output, pid).some(file => {
87
+ if (file["f"] !== "txt" || file["i"] !== inode || !/^0x[0-9a-f]+$/i.test(file["D"] ?? "")) return false;
88
+ try { return BigInt(file["D"]!) === BigInt(device); } catch { return false; }
89
+ });
90
+ }
91
+ export function lsofOwnsPort(output: string, pid: number, port: number): boolean {
92
+ return lsofFiles(output, pid).some(file => file["t"] === "IPv4" && file["P"] === "TCP" && file["TST"] === "LISTEN" &&
93
+ ["*", "127.0.0.1"].some(host => file["n"] === `${host}:${port}`));
94
+ }