@visual-z/xeno 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -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[];
@@ -82,10 +89,31 @@ export interface ProjectResult {
82
89
  }
83
90
 
84
91
  export interface ProjectViolation {
85
- code: "UNPAIRED_TOOL_USE" | "ORPHAN_TOOL_RESULT" | "EMPTY_PROJECTION";
92
+ code: "UNPAIRED_TOOL_USE" | "ORPHAN_TOOL_RESULT" | "EMPTY_PROJECTION" | "DUPLICATE_TOOL_USE" | "EMPTY_TOOL_USE_ID";
86
93
  detail: string;
87
94
  }
88
95
 
96
+ /**
97
+ * D5: a receipt identifies exactly one call in its assistant batch. Validate
98
+ * before either the projection's Set or the runtime's scheduling Map can erase
99
+ * a collision; repairing IDs would invent provider protocol history.
100
+ */
101
+ export function toolCallIdentityViolations(blocks: readonly AssistantBlock[]): ProjectViolation[] {
102
+ const violations: ProjectViolation[] = [], seen = new Map<string, number>();
103
+ for (const [index, block] of blocks.entries()) {
104
+ if (block.type !== "tool_use") continue;
105
+ if (block.call_id.trim().length === 0) {
106
+ violations.push({ code: "EMPTY_TOOL_USE_ID", detail: `tool_use block ${index} has an empty call_id` });
107
+ continue;
108
+ }
109
+ const previous = seen.get(block.call_id);
110
+ if (previous !== undefined) {
111
+ violations.push({ code: "DUPLICATE_TOOL_USE", detail: `tool_use block ${index} repeats the call_id of block ${previous}` });
112
+ } else seen.set(block.call_id, index);
113
+ }
114
+ return violations;
115
+ }
116
+
89
117
  /**
90
118
  * The only event types that replay as messages.
91
119
  *
@@ -110,9 +138,11 @@ export interface ProjectViolation {
110
138
  * two things". It is a following message rather than a mutation of the user
111
139
  * turn so appending the event only appends to the projection (P7).
112
140
  */
113
- type ReplayEvent = Extract<LogEvent, { t: "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
141
+ type ReplayEvent = Extract<LogEvent, { t: "acceptance_contract" | "acceptance_decision" | "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
114
142
 
115
143
  function replayable(e: LogEvent): e is ReplayEvent {
144
+ if (e.t === "tool_result" && (e.parent_call_id !== undefined || e.origin === "extension")) return false;
145
+ if (e.t === "acceptance_contract" || e.t === "acceptance_decision") return true;
116
146
  if (e.t === "extension") return e.inContext;
117
147
  if (e.t === "user_bash") return !e.excludeFromContext;
118
148
  if (e.t === "rewind") return e.mode !== "both";
@@ -198,7 +228,7 @@ export function renderRewindMessage(e: Extract<LogEvent, { t: "rewind" }>): Mess
198
228
  */
199
229
  export function renderHandoffMessage(
200
230
  handoff: Handoff,
201
- logHint?: { manifestPath: string; segmentPath: string },
231
+ logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true },
202
232
  ): Message {
203
233
  const yaml = emitHandoff(handoff);
204
234
 
@@ -221,8 +251,10 @@ export function renderHandoffMessage(
221
251
  "detail this state does not carry, read it rather than guessing:",
222
252
  ` - segment index: ${logHint.manifestPath}`,
223
253
  ` - 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.",
254
+ ...(logHint.scopedGuidance !== true ? [
255
+ "Each line is one JSON event. Search it with history; read a line range with read,",
256
+ "or filter with bash and jq. The grep tool skips the log directory.",
257
+ ] : ["Each line is one JSON event. Recover originals using the declared tools' contracts; if evidence is inaccessible, state the gap instead of guessing."]),
226
258
  );
227
259
  }
228
260
 
@@ -251,9 +283,11 @@ export function renderHandoffMessage(
251
283
  export function projectContext(input: ProjectInput): ProjectResult {
252
284
  const violations: ProjectViolation[] = [];
253
285
  const messages: Message[] = [];
286
+ const sources: string[][] = [];
287
+ const append = (message: Message, ids: string[]) => { messages.push(message); sources.push(ids); };
254
288
 
255
289
  if (input.handoff) {
256
- messages.push(renderHandoffMessage(input.handoff, input.logHint));
290
+ append(renderHandoffMessage(input.handoff, input.logHint), [`handoff:${input.handoff.predecessor?.node ?? "root"}`]);
257
291
  }
258
292
 
259
293
  const events = input.events.filter(replayable);
@@ -262,10 +296,12 @@ export function projectContext(input: ProjectInput): ProjectResult {
262
296
  // a SINGLE user message. Splitting them across messages teaches the model to stop
263
297
  // issuing parallel calls, which is a silent, permanent capability regression.
264
298
  let pending: ContentBlock[] = [];
299
+ let pendingSources: string[] = [];
265
300
  const flushPending = () => {
266
301
  if (pending.length > 0) {
267
- messages.push({ role: "user", content: pending });
302
+ append({ role: "user", content: pending }, pendingSources);
268
303
  pending = [];
304
+ pendingSources = [];
269
305
  }
270
306
  };
271
307
 
@@ -273,29 +309,38 @@ export function projectContext(input: ProjectInput): ProjectResult {
273
309
  let awaiting = new Set<string>();
274
310
 
275
311
  for (const e of events) {
312
+ if (e.t === "acceptance_contract" || e.t === "acceptance_decision") {
313
+ flushPending();
314
+ if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `acceptance event ${e.id} follows unanswered tools` });
315
+ append({ role: "user", content: [{ type: "text", text: e.t === "acceptance_contract"
316
+ ? `Declared acceptance contract (execution success alone does not satisfy it): ${JSON.stringify(e.contract)}`
317
+ : `User accepted criterion ${e.criterion} of contract ${e.contract_id} for request ${e.request}.` +
318
+ (e.target_binding ? ` Bound to runtime instance ${e.target_binding}, observation ${e.target_observation}. Later runtime changes invalidate this acceptance.` : "") }] }, [e.id]);
319
+ continue;
320
+ }
276
321
  if (e.t === "extension") {
277
322
  flushPending();
278
323
  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 }] });
324
+ append({ role: "user", content: [{ type: "text", text: e.content }] }, [e.id]);
280
325
  continue;
281
326
  }
282
327
  if (e.t === "user_bash") {
283
328
  flushPending();
284
- messages.push({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] });
329
+ append({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] }, [e.id]);
285
330
  continue;
286
331
  }
287
332
  if (e.t === "snapshot") {
288
333
  flushPending();
289
- messages.push({ role: "user", content: [{ type: "text",
334
+ append({ role: "user", content: [{ type: "text",
290
335
  text: `Workspace observation: changes outside declared file edits: ${e.undeclared.join(", ")}. ${REREAD_INSTRUCTION}`,
291
- }] });
336
+ }] }, [e.id]);
292
337
  continue;
293
338
  }
294
339
  if (e.t === "rewind") {
295
340
  // Flushed first for the same reason a user turn is: it opens a user
296
341
  // message, and a pending tool_result batch has to close before it.
297
342
  flushPending();
298
- messages.push(renderRewindMessage(e));
343
+ append(renderRewindMessage(e), [e.id]);
299
344
  continue;
300
345
  }
301
346
  if (e.t === "user" || e.t === "guidance") {
@@ -307,8 +352,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
307
352
  });
308
353
  awaiting = new Set();
309
354
  }
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 })) : [])] });
355
+ append({ role: "user", content: [...(e.text || e.t === "guidance" || !e.images?.length ? [{ type: "text" as const, text: e.text }] : []),
356
+ ...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] }, [e.id]);
312
357
  continue;
313
358
  }
314
359
  if (e.t === "interrupt") {
@@ -316,19 +361,22 @@ export function projectContext(input: ProjectInput): ProjectResult {
316
361
  // look-ahead mutation of that turn would rewrite it once a later
317
362
  // event (tool_use, another user) was already projected after it.
318
363
  flushPending();
319
- messages.push({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] });
364
+ append({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] }, [e.id]);
320
365
  continue;
321
366
  }
322
367
 
323
368
  if (e.t === "assistant") {
324
369
  flushPending();
370
+ violations.push(...toolCallIdentityViolations(e.blocks).map(violation => ({
371
+ ...violation, detail: `assistant turn ${e.id}: ${violation.detail}`,
372
+ })));
325
373
  if (awaiting.size > 0) {
326
374
  violations.push({
327
375
  code: "UNPAIRED_TOOL_USE",
328
376
  detail: `assistant turn ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}`,
329
377
  });
330
378
  }
331
- messages.push({ role: "assistant", content: e.blocks.map(toContentBlock) });
379
+ append({ role: "assistant", content: e.blocks.map(toContentBlock) }, [e.id]);
332
380
  awaiting = new Set(
333
381
  e.blocks.filter((b) => b.type === "tool_use").map((b) => (b as { call_id: string }).call_id),
334
382
  );
@@ -344,10 +392,11 @@ export function projectContext(input: ProjectInput): ProjectResult {
344
392
  continue;
345
393
  }
346
394
  awaiting.delete(e.call_id);
395
+ pendingSources.push(e.id);
347
396
  pending.push({
348
397
  type: "tool_result",
349
398
  tool_use_id: e.call_id,
350
- content: e.content,
399
+ content: diagnosticContent(e.content, e.diagnostic) + (e.warnings?.length ? "\nObserver warnings (operation result retained): " + JSON.stringify(e.warnings) : ""),
351
400
  ...(e.ok ? {} : { is_error: true }),
352
401
  });
353
402
  // Images follow the paired text result, so APIs without image tool outputs receive a user image.
@@ -368,6 +417,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
368
417
  }
369
418
 
370
419
  return {
420
+ ...(input.trace ? { provenance: { version: PROJECTION_VERSION, messages: sources,
421
+ omitted: input.events.filter(e => !sources.some(ids => ids.includes(e.id))).map(e => e.id) } } : {}),
371
422
  system: input.frozen.system,
372
423
  tools: input.frozen.tools,
373
424
  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,87 @@
1
+ /** Q23 / D9: diagnostic observations, never a new anti-stall or success policy. */
2
+ import type { LogEvent, ToolResultEvent } from "./events.ts";
3
+ import { deriveAcceptance, type AcceptanceState } from "./acceptance.ts";
4
+ import { advanceRecovery, emptyRecoveryState, failureIdentity } from "./failure-recovery.ts";
5
+ import { canonicalValue } from "./continuation.ts";
6
+ import { changedPaths } from "./checkpoints.ts";
7
+
8
+ export type RequestSource = { node: number; event: string };
9
+ export type RequestRecord = { node: number; event: LogEvent };
10
+ export type RequestProgress = Record<"observations" | "artifact_changes" | "acceptance_advances" |
11
+ "new_diagnostics" | "unchanged_retries" | "unknown_diagnostics", RequestSource[]>;
12
+ const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
13
+
14
+ function observationDigest(event: ToolResultEvent): string | undefined {
15
+ if (event.observation_sha256 !== undefined) return event.observation_sha256;
16
+ // Command receipts archive stdout/stderr before the timing/session header is
17
+ // rendered. Only that explicit matching output reference is a safe fallback.
18
+ const data = event.data;
19
+ if (event.effect?.kind !== "command" || !event.content_ref || !object(data) || data["capture"] === "partial") return undefined;
20
+ const output = data["output"];
21
+ return object(output) &&
22
+ output["sha256"] === event.content_ref.sha256 && output["bytes"] === event.content_ref.bytes ? event.content_ref.sha256 : undefined;
23
+ }
24
+
25
+ export function deriveRequestProgress(records: readonly RequestRecord[]): RequestProgress {
26
+ const result: RequestProgress = { observations: [], artifact_changes: [], acceptance_advances: [],
27
+ new_diagnostics: [], unchanged_retries: [], unknown_diagnostics: [] };
28
+ const observations = new Set<string>(), versions = new Set<string>(), failures = new Set<string>();
29
+ let acceptance: AcceptanceState | undefined;
30
+ let recovery = emptyRecoveryState();
31
+ const version = (path: string, before: string | null, after: string | null, source: RequestSource) => {
32
+ const key = (sha: string | null) => canonicalValue([path, sha]);
33
+ versions.add(key(before));
34
+ if (!versions.has(key(after))) result.artifact_changes.push(source);
35
+ versions.add(key(after));
36
+ };
37
+ for (const { node, event } of records) {
38
+ const source = { node, event: event.id };
39
+ const before = acceptance;
40
+ acceptance = deriveAcceptance(acceptance, [event]);
41
+ if (acceptance && before?.contract_event === acceptance.contract_event && acceptance.criteria.some(item =>
42
+ (item.status === "pass" || item.status === "accepted") &&
43
+ before.criteria.find(old => old.id === item.id)?.status !== item.status)) result.acceptance_advances.push(source);
44
+ if (event.t === "tool_result") {
45
+ const effect = event.effect;
46
+ const call = recovery.calls.get(event.call_id);
47
+ if (effect?.kind === "edit") version(effect.path, effect.sha256_before, effect.sha256_after, source);
48
+ if (effect?.kind === "read") versions.add(canonicalValue([effect.path, effect.sha256]));
49
+ // Semantic digests exclude command timing/preview metadata. Legacy raw
50
+ // output hashes cannot prove a new diagnostic and stay explicitly unknown.
51
+ const digest = observationDigest(event);
52
+ if (!event.ok) {
53
+ const identity = call ? failureIdentity(call, event, recovery, path => path) : null;
54
+ if (digest === undefined || identity === null) result.unknown_diagnostics.push(source);
55
+ else {
56
+ const key = canonicalValue([identity, digest]);
57
+ (failures.has(key) ? result.unchanged_retries : result.new_diagnostics).push(source);
58
+ failures.add(key);
59
+ }
60
+ } else if ((effect?.kind === "read" || effect?.kind === "command" ||
61
+ ["grep", "find", "ls", "query", "history", "environment"].includes(call?.name ?? "")) &&
62
+ digest !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 && !observations.has(digest)) {
63
+ result.observations.push(source); observations.add(digest);
64
+ }
65
+ if (effect?.kind === "command") for (const output of effect.artifacts?.outputs ?? []) {
66
+ if (output.after.state === "observed") version(output.path,
67
+ output.before.state === "observed" ? output.before.sha256 : null, output.after.sha256, source);
68
+ }
69
+ } else if (event.t === "checkpoint" && event.phase === "applied") {
70
+ for (const path of changedPaths(event.changes)) {
71
+ const change = event.changes[path]!;
72
+ version(path, change.before?.sha256 ?? null, change.after?.sha256 ?? null, source);
73
+ }
74
+ }
75
+ recovery = advanceRecovery(recovery, [event], path => path);
76
+ }
77
+ // One receipt may change several artifacts or satisfy several criteria.
78
+ for (const key of Object.keys(result) as (keyof RequestProgress)[]) {
79
+ const seen = new Set<string>();
80
+ result[key] = result[key].filter(source => {
81
+ const id = canonicalValue(source);
82
+ if (seen.has(id)) return false;
83
+ seen.add(id); return true;
84
+ });
85
+ }
86
+ return result;
87
+ }