@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
@@ -91,6 +91,9 @@ function escapeHtml(s: string): string {
91
91
 
92
92
  function eventHtml(e: LogEvent): string {
93
93
  switch (e.t) {
94
+ case "tool_invocation": return wrap("event", "tool invocation", e.at, `${e.name} · ${e.call_id}${e.parent_call_id ? ` · parent ${e.parent_call_id}` : ""} · input ${e.input_ref.sha256} · schema ${e.schema_ref?.sha256 ?? "unavailable"}`);
95
+ case "checkpoint": return wrap("event", `file checkpoint ${e.phase}`, e.at, e.reason ?? Object.keys(e.changes).join("\n"));
96
+ case "workspace_observation": return wrap("event", "verification observation", e.at, e.fingerprint ?? e.reason ?? "unavailable");
94
97
  case "extension": return e.display ? wrap("event", e.customType, e.at, e.content || JSON.stringify(e.data)) : "";
95
98
  case "user":
96
99
  return wrap("user", "user", e.at, [e.text, ...(e.images ?? []).map(imageDescription)].join("\n"));
@@ -143,6 +146,12 @@ function eventHtml(e: LogEvent): string {
143
146
  return wrap("event", `work ${e.operation}`, e.at, `${e.work_id}: ${e.what}`);
144
147
  case "evidence_link":
145
148
  return wrap("event", "evidence link", e.at, `${e.work}: ${e.what} → ${e.source}`);
149
+ case "work_owner": case "work_dependencies": return wrap("event", e.t, e.at, JSON.stringify(e));
150
+ case "skill_loaded": return wrap("event", "skill loaded", e.at, `${e.name} · ${e.path} · ${e.sha256} · ${e.mode}`);
151
+ case "process_exit": return wrap("event", "process exit", e.at, `${e.session} · exit ${e.exit} · output ${e.output.sha256}`);
152
+ case "acceptance_contract": return wrap("event", "acceptance contract", e.at, JSON.stringify(e.contract));
153
+ case "target_observation": return wrap("event", "runtime target", e.at, JSON.stringify(e.observation));
154
+ case "acceptance_decision": return wrap("event", "human acceptance", e.at, `${e.contract_id}: ${e.criterion}`);
146
155
  default:
147
156
  return assertNever(e, "eventHtml");
148
157
  }
@@ -0,0 +1,173 @@
1
+ /** R5 / D8: replayable failure premises; advice never authorizes or automatically retries a tool. */
2
+ import type { GuidanceEvent, LogEvent, ToolResultEvent } from "./events.ts";
3
+ import type { ToolDiagnostic } from "./tool-contract.ts";
4
+ import { canonicalValue } from "./continuation.ts";
5
+ import { repairToolInput } from "./tool-input.ts";
6
+ import { failureDiagnostic, scopeFailureGuidance } from "./tool-failure.ts";
7
+
8
+ /** Reuses the existing three-attempt diagnostic hypothesis, not a measured optimum or a stop threshold. */
9
+ export const RECOVERY_FAILURES = 3;
10
+ type Call = { name: string; input: unknown };
11
+ export interface FailureIdentity {
12
+ family: string;
13
+ resource: string;
14
+ version: string;
15
+ code: ToolDiagnostic["code"];
16
+ execution: NonNullable<ToolDiagnostic["execution"]>;
17
+ effects: NonNullable<ToolDiagnostic["effects"]>;
18
+ premise: string;
19
+ }
20
+ interface FailureRun {
21
+ identity: FailureIdentity;
22
+ key: string;
23
+ count: number;
24
+ sources: readonly string[];
25
+ notified: boolean;
26
+ }
27
+ export interface RecoveryState {
28
+ calls: ReadonlyMap<string, Call>;
29
+ versions: ReadonlyMap<string, string>;
30
+ failures: ReadonlyMap<string, FailureRun>;
31
+ change: number;
32
+ }
33
+ export function emptyRecoveryState(): RecoveryState {
34
+ return { calls: new Map(), versions: new Map(), failures: new Map(), change: 0 };
35
+ }
36
+ const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
37
+ const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
38
+ const fileTools = ["read", "write", "edit", "grep", "find", "ls"];
39
+ const family = (name: string): string => name === "bash" || name === "exec_command" ? "command"
40
+ : name === "edit" || name === "write" ? "file_change" : name;
41
+
42
+ function target(call: Call, identify: (path: string) => string): { family: string; resource: string; input: Record<string, unknown> } {
43
+ const repaired = repairToolInput(call.name, call.input), input = record(repaired) ? repaired : { value: repaired };
44
+ const kind = family(call.name);
45
+ return { family: kind, input, resource: fileTools.includes(call.name) && typeof input["path"] === "string" ? identify(input["path"])
46
+ : kind === "command" ? identify(typeof input["workdir"] === "string" ? input["workdir"] : ".")
47
+ : call.name === "write_stdin" ? `session:${String(input["session_id"])}` : call.name };
48
+ }
49
+
50
+ function groupKey(call: Call, result: ToolResultEvent, identify: (path: string) => string): string {
51
+ const context = target(call, identify);
52
+ return canonicalValue([context.family, context.resource, context.family === "command"
53
+ ? result.effect?.kind === "command" ? result.effect.cmd.trim() : String(context.input["command"] ?? context.input["cmd"] ?? "").trim() : null]);
54
+ }
55
+
56
+ /** Only recorded versions and structured failures are facts; stderr words never identify an error. */
57
+ export function failureIdentity(call: Call, result: ToolResultEvent, state: RecoveryState,
58
+ identify: (path: string) => string): FailureIdentity | null {
59
+ const diagnostic = result.diagnostic;
60
+ if (result.ok || !diagnostic || diagnostic.execution === undefined || diagnostic.effects === undefined) return null;
61
+ const context = target(call, identify), data = record(result.data) ? result.data : {};
62
+ const current = data["sha256"];
63
+ const effect = result.effect;
64
+ const known = state.versions.get(context.resource);
65
+ const version = diagnostic.code === "permission_denied" ? "unknown" : current === null ? "absent" : digest(current) ? current
66
+ : effect?.kind === "command" && effect.workspace_observed !== false && digest(effect.workspace_sha256) ? effect.workspace_sha256
67
+ : digest(known) ? known : "unknown";
68
+ const input = context.input;
69
+ let premise: unknown;
70
+ if (diagnostic.code === "version_mismatch") {
71
+ const precondition = record(data["precondition"]) ? data["precondition"] : {};
72
+ premise = { expected: precondition["expected"] === null ? "absent"
73
+ : precondition["expected"] ?? input["expected_sha256"] ?? input["read_handle"] ?? "unknown" };
74
+ } else if (diagnostic.code === "permission_denied") {
75
+ // Replacement wording and requested shell timeouts cannot change a denied capability.
76
+ premise = { operation: context.family === "command" ? effect?.kind === "command" ? effect.cmd.trim()
77
+ : String(input["command"] ?? input["cmd"] ?? "").trim() : context.family };
78
+ } else if (context.family === "file_change" && ["not_found", "ambiguous_edit"].includes(diagnostic.code)) {
79
+ const edits = Array.isArray(input["edits"]) ? input["edits"] : [input];
80
+ premise = edits.map(edit => record(edit) ? { old_string: edit["old_string"], replace_all: edit["replace_all"] === true } : edit);
81
+ } else if (context.family === "command") {
82
+ // Shared launch contract equates bash/exec_command; arbitrary shell strings are not parsed as equivalent.
83
+ premise = { command: effect?.kind === "command" ? effect.cmd.trim() : String(input["command"] ?? input["cmd"] ?? "").trim(),
84
+ exit: effect?.kind === "command" ? effect.exit : null,
85
+ timeout: input["timeout"] ?? null, tty: input["tty"] === true, change: state.change };
86
+ } else {
87
+ const normalized = { ...input };
88
+ if (fileTools.includes(call.name) && typeof normalized["path"] === "string") normalized["path"] = context.resource;
89
+ premise = normalized;
90
+ }
91
+ return { family: context.family, resource: context.resource, version, code: diagnostic.code,
92
+ execution: diagnostic.execution, effects: diagnostic.effects, premise: canonicalValue(premise) };
93
+ }
94
+
95
+ function changedPremises(state: RecoveryState, changed: boolean, resource?: string, version?: string): RecoveryState {
96
+ return { ...state, change: state.change + Number(changed), failures: new Map([...state.failures].filter(([, run]) =>
97
+ run.identity.code === "permission_denied" || !(changed && run.identity.family === "command" ||
98
+ resource !== undefined && version !== undefined && run.identity.resource === resource && run.identity.version !== version))) };
99
+ }
100
+
101
+ /** Incremental disposable projection; folding the original ancestry yields exactly the same state. */
102
+ export function advanceRecovery(state: RecoveryState, events: readonly LogEvent[], identify: (path: string) => string): RecoveryState {
103
+ let current = state;
104
+ for (const event of events) {
105
+ if (event.t === "user") { current = { ...current, failures: new Map() }; continue; }
106
+ if (event.t === "assistant") {
107
+ const calls = new Map(current.calls);
108
+ for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, { name: block.name, input: block.input });
109
+ current = { ...current, calls }; continue;
110
+ }
111
+ if (event.t === "tool_invocation") {
112
+ if (event.input !== undefined) current = { ...current, calls: new Map(current.calls).set(event.call_id, { name: event.name, input: event.input }) };
113
+ continue;
114
+ }
115
+ if (event.t === "guidance" && event.reason === "churn") {
116
+ current = { ...current, failures: new Map([...current.failures].map(([group, run]) =>
117
+ [group, event.sources.includes(run.sources[0]!) ? { ...run, notified: true } : run])) }; continue;
118
+ }
119
+ if (event.t === "workspace_observation" && event.changed) {
120
+ current = changedPremises(current, true); continue;
121
+ }
122
+ if (event.t === "checkpoint" && event.phase === "applied") {
123
+ for (const [path, change] of Object.entries(event.changes))
124
+ current = changedPremises(current, change.before?.sha256 !== change.after?.sha256 || change.before?.mode !== change.after?.mode,
125
+ identify(path), change.after === null ? "absent" : change.after.sha256);
126
+ continue;
127
+ }
128
+ if (event.t !== "tool_result") continue;
129
+ const call = current.calls.get(event.call_id), calls = new Map(current.calls); calls.delete(event.call_id);
130
+ current = { ...current, calls };
131
+ const effect = event.effect;
132
+ if (effect?.kind === "edit" || effect?.kind === "read") {
133
+ const resource = identify(effect.path), previous = current.versions.get(resource);
134
+ const version = effect.kind === "edit" ? effect.sha256_after : effect.sha256;
135
+ const changed = effect.kind === "edit" ? effect.sha256_before !== effect.sha256_after : digest(previous) && previous !== version;
136
+ current = changedPremises({ ...current, versions: new Map(current.versions).set(resource, version) }, changed,
137
+ resource, digest(version) ? version : undefined);
138
+ }
139
+ if (!call) continue;
140
+ const group = groupKey(call, event, identify);
141
+ if (event.ok) {
142
+ const failures = new Map(current.failures); failures.delete(group);
143
+ current = { ...current, failures }; continue;
144
+ }
145
+ const identity = failureIdentity(call, event, current, identify);
146
+ if (!identity) continue;
147
+ const key = canonicalValue(identity), previous = current.failures.get(group);
148
+ const run: FailureRun = previous?.key === key ? { ...previous, count: previous.count + 1,
149
+ sources: [...previous.sources.slice(0, 2), event.id] }
150
+ : { identity, key, count: 1, sources: [event.id], notified: false };
151
+ current = { ...current, failures: new Map(current.failures).set(group, run) };
152
+ }
153
+ return current;
154
+ }
155
+
156
+ export function recoveryReview(state: RecoveryState, tools?: readonly string[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
157
+ for (const run of state.failures.values()) {
158
+ if (run.count < RECOVERY_FAILURES || run.notified) continue;
159
+ const identity = run.identity;
160
+ // Recompute trusted instructions; never promote arbitrary tool error text to harness instructions.
161
+ const diagnostic = failureDiagnostic(identity.code, "", { execution: identity.execution, effects: identity.effects });
162
+ const policy = tools === undefined ? diagnostic : scopeFailureGuidance(diagnostic, tools);
163
+ return { reason: "churn", sources: run.sources,
164
+ text: `Harness: ${run.count} attempts share an unchanged failure premise in the recorded facts. ` +
165
+ `Tool family ${JSON.stringify(identity.family)}; resource label (data) ${JSON.stringify(identity.resource.slice(0, 160))}; ` +
166
+ `code ${identity.code}; version ${identity.version}; execution ${identity.execution}; effects ${identity.effects}. ` +
167
+ `${policy.next} ` + (policy.retry === "after_inspection" ? "Inspect actual state for partial effects before any replay. " : "") +
168
+ "Identify which relevant premise changed before another attempt; changing replacement wording is not a new premise. " +
169
+ (identity.version === "unknown" ? "The resource version is unknown; this notice does not prove external state is unchanged. " : "") +
170
+ "This is recovery advice, not a task-failure verdict or authorization to replay. Original result references: " + run.sources.join(", ") + "." };
171
+ }
172
+ return null;
173
+ }
@@ -0,0 +1,72 @@
1
+ /** R4 / D8: a read descriptor names immutable version premises; it grants no authority. */
2
+ import type { ObservationReceipt } from "./read-observation.ts";
3
+ import { isObservationArtifact } from "./observation-view.ts";
4
+ import type { LogEvent } from "./events.ts";
5
+
6
+ export interface FileIdentity { host: string; workspace: string; resource: string }
7
+ export interface FileReadDescriptor {
8
+ kind: "file_read";
9
+ version: 1;
10
+ source: ObservationReceipt["source"];
11
+ selection: ObservationReceipt["selection"];
12
+ }
13
+ export interface FilePrecondition {
14
+ source: "read_handle" | "expected_sha256" | "session" | "create_only" | "unobserved";
15
+ expected: string | null;
16
+ read_handle?: string;
17
+ }
18
+ export type FilePreconditionDecision = { ok: true; precondition: FilePrecondition } |
19
+ { ok: false; code: "invalid_input" | "version_mismatch"; message: string; precondition?: FilePrecondition };
20
+
21
+ const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
22
+ const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
23
+ export const isFileDigest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
24
+
25
+ /** Preserve chronology across path aliases; handoff anchors intentionally keep their legacy spelling. */
26
+ export function latestObservedFileVersion(events: readonly LogEvent[], resource: string, identify: (path: string) => string): string | undefined {
27
+ for (let index = events.length - 1; index >= 0; index--) {
28
+ const event = events[index]!;
29
+ if (event.t !== "tool_result") continue;
30
+ const effect = event.effect;
31
+ if ((effect?.kind === "read" || effect?.kind === "edit") && identify(effect.path) === resource)
32
+ return effect.kind === "read" ? effect.sha256 : effect.sha256_after;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ export function isFileReadDescriptor(value: unknown): value is FileReadDescriptor {
38
+ if (!record(value) || value["kind"] !== "file_read" || value["version"] !== 1) return false;
39
+ const source = value["source"], selection = value["selection"];
40
+ return record(source) && ["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") &&
41
+ isFileDigest(source["sha256"]) && isObservationArtifact(source["raw"]) && source["raw"].sha256 === source["sha256"] &&
42
+ record(selection) && natural(selection["start"]) && selection["start"] >= 1 && natural(selection["end"]) &&
43
+ natural(selection["total_lines"]) && selection["start"] <= selection["end"] && selection["end"] <= selection["total_lines"];
44
+ }
45
+
46
+ /** Explicit premises take precedence; an omitted premise may use the complete log projection. */
47
+ export function checkFilePrecondition(identity: FileIdentity, current: string | null, input: {
48
+ descriptor?: FileReadDescriptor; read_handle?: string; expected_sha256?: string; observed?: string; create_only?: boolean;
49
+ }): FilePreconditionDecision {
50
+ const invalid = (message: string): FilePreconditionDecision => ({ ok: false, code: "invalid_input", message });
51
+ if (input.expected_sha256 !== undefined && !isFileDigest(input.expected_sha256)) return invalid("expected_sha256 must be a lowercase SHA-256 digest");
52
+ if (input.observed !== undefined && !isFileDigest(input.observed)) return invalid("the observed file version is invalid; inspect the original log");
53
+ if (input.create_only && (input.read_handle !== undefined || input.expected_sha256 !== undefined))
54
+ return invalid("create_only cannot be combined with read_handle or expected_sha256");
55
+ if ((input.read_handle === undefined) !== (input.descriptor === undefined)) return invalid("read_handle requires its verified descriptor");
56
+ if (input.descriptor) {
57
+ if (!isFileReadDescriptor(input.descriptor) || !isFileDigest(input.read_handle)) return invalid("invalid read_handle descriptor");
58
+ for (const field of ["host", "workspace", "resource"] as const)
59
+ if (input.descriptor.source[field] !== identity[field]) return invalid(`read_handle names another ${field}; read the intended file here`);
60
+ if (input.expected_sha256 !== undefined && input.expected_sha256 !== input.descriptor.source.sha256)
61
+ return invalid("read_handle and expected_sha256 name different versions");
62
+ }
63
+ const precondition: FilePrecondition = input.create_only ? { source: "create_only", expected: null }
64
+ : input.descriptor ? { source: "read_handle", expected: input.descriptor.source.sha256, read_handle: input.read_handle! }
65
+ : input.expected_sha256 !== undefined ? { source: "expected_sha256", expected: input.expected_sha256 }
66
+ : input.observed !== undefined ? { source: "session", expected: input.observed }
67
+ : { source: "unobserved", expected: null };
68
+ if (precondition.source !== "unobserved" && precondition.expected !== current)
69
+ return { ok: false, code: "version_mismatch", precondition,
70
+ message: `file version changed; expected ${precondition.expected ?? "absent"}, current sha256: ${current ?? "absent"}. Read the current file before retrying.` };
71
+ return { ok: true, precondition };
72
+ }
@@ -19,6 +19,11 @@ import type { ProgressEvidence } from "./progress.ts";
19
19
  import { isExecutionOutcome, type ExecutionOutcome } from "./outcome.ts";
20
20
  import type { WorkItem } from "./work-state.ts";
21
21
  import type { EvidenceLink } from "./evidence-links.ts";
22
+ import type { WorkingSet } from "./working-set.ts";
23
+ import { RECOVERY_SECTIONS, RECOVERY_KINDS } from "./recovery-sources.ts";
24
+ import type { AcceptanceState } from "./acceptance.ts";
25
+ import type { LoadedSkill } from "./skills.ts";
26
+ import type { ProcessObservation } from "./process-state.ts";
22
27
  import { emitYaml } from "./yaml.ts";
23
28
 
24
29
  export const HANDOFF_VERSION = 1 as const;
@@ -34,6 +39,8 @@ export type Evidence = {
34
39
  event_id: string;
35
40
  /** Exact observed workspace after the command; absence means legacy/unknown. */
36
41
  workspace_sha256?: string;
42
+ /** P.3: omitted on historical full-workspace observations. */
43
+ workspace_scope?: "source-v1";
37
44
  }
38
45
 
39
46
  export type CompletedItem = {
@@ -79,12 +86,16 @@ export type BlockedItem = {
79
86
  }
80
87
 
81
88
  export type DecisionItem = {
89
+ work_id?: string;
90
+ source?: { node: number; event_id: string };
82
91
  chose: string;
83
92
  because: string;
84
93
  rejected?: readonly string[];
85
94
  }
86
95
 
87
96
  export type PitfallItem = {
97
+ work_id?: string;
98
+ source?: { node: number; event_id: string };
88
99
  tried: string;
89
100
  failed_because: string;
90
101
  }
@@ -110,11 +121,13 @@ export type BranchRecord = {
110
121
  /** Event id in this node where the branch was spawned. */
111
122
  spawned_at: EventId;
112
123
  purpose: string;
124
+ background?: true;
113
125
  /** null means unresolved, which D16 requires be visible rather than quiet. */
114
126
  resolution: "merged" | "discarded" | null;
115
127
  /** Present once the branch joined; a path, never the handoff's contents. */
116
128
  handoff_ref?: string;
117
129
  summary?: string;
130
+ receipt_ref?: string;
118
131
  outcome?: ExecutionOutcome;
119
132
  };
120
133
 
@@ -126,6 +139,7 @@ export type VerificationItem = {
126
139
  at?: IsoTime;
127
140
  exit?: number;
128
141
  workspace_sha256?: string;
142
+ workspace_scope?: "source-v1";
129
143
  stale?: true;
130
144
  }
131
145
 
@@ -142,6 +156,11 @@ export type UserTurnRecord = {
142
156
 
143
157
  export type Handoff = {
144
158
  handoff_version: typeof HANDOFF_VERSION | 2;
159
+ working_set?: WorkingSet;
160
+ acceptance?: AcceptanceState;
161
+ loaded_skills?: readonly LoadedSkill[];
162
+ /** O.14: last observations only; live capabilities cannot survive a restart. */
163
+ processes?: readonly ProcessObservation[];
145
164
  /** V2 arrays are carried suffixes; the immutable source rebuilds every page. */
146
165
  history?: {
147
166
  source: NodeRef;
@@ -219,8 +238,8 @@ export type Handoff = {
219
238
  const ORDER: Record<string, readonly string[]> = {
220
239
  "": [
221
240
  "handoff_version", "predecessor", "history",
222
- "objective", "user_turns", "user_turns_truncated",
223
- "state", "decisions", "pitfalls", "file_anchors",
241
+ "objective", "working_set", "user_turns", "user_turns_truncated",
242
+ "state", "acceptance", "loaded_skills", "processes", "decisions", "pitfalls", "file_anchors",
224
243
  "verification", "branches", "open_questions", "progress_evidence", "evidence_links",
225
244
  "continuation_valid", "budget",
226
245
  ],
@@ -239,7 +258,9 @@ const ORDER: Record<string, readonly string[]> = {
239
258
  pitfalls: ["tried", "failed_because"],
240
259
  file_anchors: ["path", "sha256", "read_ranges", "touched"],
241
260
  verification: ["gate", "status", "at", "exit"],
242
- branches: ["node", "spawned_at", "purpose", "resolution", "handoff_ref", "summary", "outcome"],
261
+ branches: ["node", "spawned_at", "purpose", "resolution", "handoff_ref", "summary", "outcome", "receipt_ref", "background"],
262
+ processes: ["session", "node", "event", "observed", "command", "exit", "output"],
263
+ "processes.output": ["sha256", "path", "bytes"],
243
264
  "branches.outcome": ["status", "reason"],
244
265
  evidence_links: ["id", "work", "what", "event", "resource", "version", "freshness", "superseded_by"],
245
266
  budget: ["nodes_on_path", "nodes_without_progress"],
@@ -279,6 +300,10 @@ export type Violation = {
279
300
  | "INVALID_EXECUTION_OUTCOME"
280
301
  | "INVALID_WORK_STATE"
281
302
  | "INVALID_EVIDENCE_LINK"
303
+ | "INVALID_WORKING_SET"
304
+ | "INVALID_ACCEPTANCE"
305
+ | "INVALID_SKILL_SOURCE"
306
+ | "INVALID_PROCESS_STATE"
282
307
  | "BAD_VERSION";
283
308
  detail: string;
284
309
  }
@@ -297,6 +322,10 @@ export function validateHandoff(h: Handoff): Violation[] {
297
322
  }
298
323
  const workIds = new Set<string>();
299
324
  for (const item of h.state.work_items ?? []) {
325
+ if (item.depends_on && (new Set(item.depends_on).size !== item.depends_on.length || item.depends_on.includes(item.id) ||
326
+ item.depends_on.some(dep => !dep.trim())) || item.owner !== undefined && (!item.owner.trim() || !item.owner_event)) {
327
+ v.push({ code: "INVALID_WORK_STATE", detail: `invalid dependencies or ownership for work ${item.id}` });
328
+ }
300
329
  const proven = item.verification === "verified" || item.verification === "stale";
301
330
  if (!item.id.trim() || workIds.has(item.id) || !item.what.trim() || !item.opened_at || !item.updated_at ||
302
331
  !["active", "blocked", "completed", "cancelled"].includes(item.status) ||
@@ -318,13 +347,29 @@ export function validateHandoff(h: Handoff): Violation[] {
318
347
  if (h.handoff_version !== HANDOFF_VERSION && h.handoff_version !== 2) {
319
348
  v.push({ code: "BAD_VERSION", detail: `expected ${HANDOFF_VERSION}, got ${h.handoff_version}` });
320
349
  }
350
+ if (h.working_set) {
351
+ const view = h.working_set;
352
+ if (![1, 2].includes(view.version) || view.source.node !== h.predecessor?.node || view.source.sha256 !== h.predecessor?.sha256 ||
353
+ !Number.isSafeInteger(view.omitted.work) || view.omitted.work < 0 || !Number.isSafeInteger(view.omitted.references) || view.omitted.references < 0 ||
354
+ new Set(view.work.map(item => item.id)).size !== view.work.length || view.work.some(item => !["active", "blocked"].includes(item.status)) ||
355
+ view.references.some(reference => !reference.event) ||
356
+ view.version === 2 && (!view.recovery_sections || RECOVERY_SECTIONS.some(section => {
357
+ const range = view.recovery_sections![section], references = view.references.filter(ref => ref.kind === RECOVERY_KINDS[section]);
358
+ return !range || !Number.isSafeInteger(range.total) || !Number.isSafeInteger(range.omitted) || range.omitted < 0 || range.omitted > range.total ||
359
+ range.total - range.omitted !== references.length || new Set(references.map(ref => ref.event)).size !== references.length ||
360
+ references.some(ref => !Number.isSafeInteger(ref.node) || Number(ref.node) < 0 || ref.provenance !==
361
+ (section === "users" ? "user_message" : section === "next_actions" ? "model_annotation" : "observation"));
362
+ }))) {
363
+ v.push({ code: "INVALID_WORKING_SET", detail: "working set must reference its source and explicit active state" });
364
+ }
365
+ }
321
366
  if (h.handoff_version === 2) {
322
367
  const history = h.history;
323
368
  if (!history || !h.predecessor || history.source.node !== h.predecessor.node ||
324
369
  history.source.sha256 !== h.predecessor.sha256) {
325
370
  v.push({ code: "INVALID_HISTORY", detail: "paged handoff must identify its immutable predecessor" });
326
371
  } else for (const [section, range] of Object.entries(history.sections)) {
327
- const values = section.startsWith("state.") ? h.state[section.slice(6) as keyof Handoff["state"]]
372
+ const values = section === "acceptance.criteria" ? h.acceptance?.criteria : section === "acceptance.targets" ? h.acceptance?.targets : section.startsWith("state.") ? h.state[section.slice(6) as keyof Handoff["state"]]
328
373
  : h[section as keyof Handoff];
329
374
  if (!Array.isArray(values) || !Number.isSafeInteger(range.total) || !Number.isSafeInteger(range.offset) ||
330
375
  range.offset < 0 || range.total < range.offset || range.total - range.offset !== values.length) {
@@ -333,6 +378,22 @@ export function validateHandoff(h: Handoff): Violation[] {
333
378
  }
334
379
  }
335
380
 
381
+ if (h.acceptance?.criteria.some(item => item.status === "pass" && (!item.evidence || item.evidence.exit !== 0) ||
382
+ item.status === "accepted" && (item.kind !== "human" || !item.accepted_event || item.target && !item.accepted_binding))) {
383
+ v.push({ code: "INVALID_ACCEPTANCE", detail: "criteria need actual command or user decision evidence" });
384
+ }
385
+ if (h.acceptance?.targets?.some(target => target.status === "ready" && (!target.observation || !target.binding || !target.session || !target.pid || !target.entry || !target.build_event))) {
386
+ v.push({ code: "INVALID_ACCEPTANCE", detail: "ready targets need original build and owned instance observations" });
387
+ }
388
+ if (h.loaded_skills?.some(item => !/^[a-f0-9]{64}$/.test(item.sha256) || item.sha256 !== item.artifact ||
389
+ !item.path || !item.event || !Number.isSafeInteger(item.node) || item.node < 0)) {
390
+ v.push({ code: "INVALID_SKILL_SOURCE", detail: "loaded skill must reference original instruction bytes and event" });
391
+ }
392
+ if (h.processes?.some(item => !Number.isSafeInteger(item.session) || item.session < 1 || !Number.isSafeInteger(item.node) || item.node < 0 ||
393
+ !item.event || !["running", "exited"].includes(item.observed) || item.observed === "exited" && !Number.isInteger(item.exit) ||
394
+ item.output && (!/^[a-f0-9]{64}$/.test(item.output.sha256) || !Number.isSafeInteger(item.output.bytes) || item.output.bytes < 0 || !item.output.path))) {
395
+ v.push({ code: "INVALID_PROCESS_STATE", detail: "process state requires original observation and actual exit evidence" });
396
+ }
336
397
  for (const c of h.state.completed) {
337
398
  if (c.evidence.exit !== 0) {
338
399
  v.push({
@@ -2,8 +2,9 @@
2
2
  import type { LogEvent, ToolResultEvent } from "./events.ts";
3
3
  import { canonicalValue } from "./continuation.ts";
4
4
  import { deriveEvidenceLinks, observationResources, resourceKey, type EvidenceLink } from "./evidence-links.ts";
5
+ import type { EnvironmentValidity } from "./environment.ts";
5
6
 
6
- export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome" | "data" | "diagnostic"> {
7
+ export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome" | "data" | "data_ref" | "diagnostic" | "observation" | "warnings"> {
7
8
  node: number;
8
9
  event: string;
9
10
  kind: string;
@@ -16,27 +17,62 @@ export interface HistoryEntry extends Pick<ToolResultEvent, "effect" | "outcome"
16
17
  work_ids?: readonly string[];
17
18
  evidence?: readonly EvidenceLink[];
18
19
  source_available?: boolean;
20
+ /** Runtime consumption view only; never changes the original capability receipt. */
21
+ environment?: EnvironmentValidity;
22
+ invocation?: { event: string; input: string; schema: string | null; parent_call_id?: string; origin?: "extension" };
23
+ }
24
+
25
+ /** O.15: bump when historyEntries or freshness semantics change. */
26
+ export const HISTORY_INDEX_VERSION = 6;
27
+ export function historyKeyInput(nodes: readonly { node: number; events: readonly LogEvent[] }[], version = HISTORY_INDEX_VERSION): string {
28
+ const calls = new Map<string, string>();
29
+ const self = (name: string | undefined) => name === "history" || name === "note";
30
+ return canonicalValue({ version, nodes: nodes.map(part => ({ node: part.node, events: part.events.flatMap<LogEvent>(event => {
31
+ // The index deliberately excludes its own reads. Effects still affect
32
+ // freshness, and annotations have their own durable events (O.6).
33
+ if (event.t === "tool_invocation") { calls.set(event.call_id, event.name); return self(event.name) ? [] : [event]; }
34
+ if (event.t === "tool_result" && self(calls.get(event.call_id)) && event.effect === undefined) return [];
35
+ if (event.t !== "assistant") return [event];
36
+ for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, block.name);
37
+ const blocks = event.blocks.filter(block => block.type === "text" || block.type === "tool_use" && !self(block.name));
38
+ return blocks.length ? [{ ...event, blocks }] : [];
39
+ }) })) });
19
40
  }
20
41
 
21
42
  export function historyEntries(nodes: readonly { node: number; events: readonly LogEvent[] }[]): HistoryEntry[] {
22
43
  const entries: HistoryEntry[] = [];
23
44
  const calls = new Map<string, { name: string; input: unknown }>();
45
+ const invocations = new Map<string, NonNullable<HistoryEntry["invocation"]>>();
24
46
  for (const { node, events } of nodes) for (const e of events) {
25
47
  if (e.t === "assistant") {
26
48
  for (const block of e.blocks) if (block.type === "tool_use") calls.set(block.call_id, { name: block.name, input: block.input });
27
49
  const text = e.blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
28
50
  if (text) entries.push({ node, event: e.id, kind: e.t, text });
51
+ } else if (e.t === "tool_invocation") {
52
+ calls.set(e.call_id, { name: e.name, input: e.input ?? calls.get(e.call_id)?.input ?? { artifact: e.input_ref.sha256 } });
53
+ invocations.set(e.call_id, { event: e.id, input: e.input_ref.sha256, schema: e.schema_ref?.sha256 ?? null,
54
+ ...(e.origin ? { origin: e.origin } : {}),
55
+ ...(e.parent_call_id ? { parent_call_id: e.parent_call_id } : {}) });
29
56
  } else if (e.t === "tool_result") {
30
57
  const call = calls.get(e.call_id);
31
58
  // Indexing query results recursively would make the index amplify itself.
32
59
  if (call?.name === "history" || call?.name === "note") continue;
33
60
  entries.push({ node, event: e.id, kind: e.t, text: e.content, ok: e.ok,
34
61
  ...(call ? { tool: call.name, input: call.input } : {}),
62
+ ...(invocations.has(e.call_id) ? { invocation: invocations.get(e.call_id)! } : {}),
35
63
  resources: observationResources(e, call?.input),
36
64
  ...(e.effect ? { effect: e.effect } : {}), ...(e.outcome ? { outcome: e.outcome } : {}),
37
65
  ...(e.data !== undefined ? { data: e.data } : {}), ...(e.diagnostic ? { diagnostic: e.diagnostic } : {}),
66
+ ...(e.data_ref ? { data_ref: e.data_ref } : {}),
67
+ ...(e.observation ? { observation: e.observation } : {}),
68
+ ...(e.warnings ? { warnings: e.warnings } : {}),
38
69
  ...(e.content_ref ? { artifact: e.content_ref.sha256 } : {}) });
39
- } else if (["user", "next_action", "decision", "pitfall", "blocked", "question", "claim", "work_status", "evidence_link"].includes(e.t)) {
70
+ } else if (e.t === "target_observation") {
71
+ entries.push({ node, event: e.id, kind: e.t, text: canonicalValue(e.observation),
72
+ ...(e.observation.manifest ? { artifact: e.observation.manifest.sha256 } : {}) });
73
+ } else if (e.t === "process_exit") {
74
+ entries.push({ node, event: e.id, kind: e.t, text: canonicalValue(e), artifact: e.output.sha256 });
75
+ } else if (["work_owner", "work_dependencies", "skill_loaded", "acceptance_contract", "acceptance_decision", "user", "next_action", "decision", "pitfall", "blocked", "question", "claim", "work_status", "evidence_link"].includes(e.t)) {
40
76
  entries.push({ node, event: e.id, kind: e.t, text: e.t === "user" ? e.text : canonicalValue(e),
41
77
  ...("work_id" in e && e.work_id ? { work_ids: [e.work_id] } : {}),
42
78
  ...(e.t === "evidence_link" ? { work_ids: [e.work], ...(e.resource ? { resources: [resourceKey(e.resource)] } : {}) } : {}) });
@@ -1,6 +1,8 @@
1
1
  /** D20: bounded carried state, with deterministic pages rebuilt from the log. */
2
2
  import { emitHandoff, type Handoff } from "./handoff.ts";
3
3
  import { estimateTokens } from "./tokens.ts";
4
+ import { workingSet, recoveryWorkingSet } from "./working-set.ts";
5
+ import type { RecoveryNodes } from "./recovery-sources.ts";
4
6
 
5
7
  // A carried handoff must leave room for another segment's work. This budget is
6
8
  // the existing 5k work floor, not a new segment threshold or an accuracy claim.
@@ -11,6 +13,10 @@ export const HISTORY_SECTIONS = [
11
13
  "branches", "open_questions", "progress_evidence",
12
14
  "state.work_items",
13
15
  "evidence_links",
16
+ "acceptance.criteria",
17
+ "acceptance.targets",
18
+ "loaded_skills",
19
+ "processes",
14
20
  ] as const;
15
21
  export type HistorySection = (typeof HISTORY_SECTIONS)[number];
16
22
 
@@ -22,6 +28,10 @@ export function historyItems(h: Handoff, section: HistorySection): readonly unkn
22
28
  case "state.blocked": return h.state.blocked;
23
29
  case "state.work_items": return h.state.work_items ?? [];
24
30
  case "evidence_links": return h.evidence_links ?? [];
31
+ case "acceptance.criteria": return h.acceptance?.criteria ?? [];
32
+ case "acceptance.targets": return h.acceptance?.targets ?? [];
33
+ case "loaded_skills": return h.loaded_skills ?? [];
34
+ case "processes": return h.processes ?? [];
25
35
  case "progress_evidence": return h.progress_evidence ?? [];
26
36
  default: return h[section];
27
37
  }
@@ -47,6 +57,10 @@ function suffixes(h: Handoff, offsets: Record<HistorySection, number>): Handoff
47
57
  open_questions: h.open_questions.slice(offsets.open_questions),
48
58
  progress_evidence: (h.progress_evidence ?? []).slice(offsets.progress_evidence),
49
59
  ...(h.evidence_links !== undefined ? { evidence_links: h.evidence_links.slice(offsets.evidence_links ?? 0) } : {}),
60
+ ...(h.acceptance ? { acceptance: { ...h.acceptance, criteria: h.acceptance.criteria.slice(offsets["acceptance.criteria"] ?? 0),
61
+ ...(h.acceptance.targets ? { targets: h.acceptance.targets.slice(offsets["acceptance.targets"] ?? 0) } : {}) } } : {}),
62
+ ...(h.loaded_skills ? { loaded_skills: h.loaded_skills.slice(offsets.loaded_skills ?? 0) } : {}),
63
+ ...(h.processes ? { processes: h.processes.slice(offsets.processes ?? 0) } : {}),
50
64
  };
51
65
  }
52
66
 
@@ -57,7 +71,11 @@ export function boundHandoff(full: Handoff, budget = DEFAULT_HANDOFF_TOKENS): Ha
57
71
  if (full.predecessor === null) throw new Error("history requires an immutable source");
58
72
  // An absent new section must not change legacy projections or frozen replay.
59
73
  const sections = HISTORY_SECTIONS.filter(section => (section !== "state.work_items" || full.state.work_items !== undefined) &&
60
- (section !== "evidence_links" || full.evidence_links !== undefined));
74
+ (section !== "evidence_links" || full.evidence_links !== undefined) &&
75
+ (section !== "acceptance.criteria" || full.acceptance !== undefined) &&
76
+ (section !== "acceptance.targets" || full.acceptance?.targets !== undefined) &&
77
+ (section !== "loaded_skills" || full.loaded_skills !== undefined) &&
78
+ (section !== "processes" || full.processes !== undefined));
61
79
  const offsets = Object.fromEntries(sections.map((s) => [s, historyItems(full, s).length])) as Record<HistorySection, number>;
62
80
  const build = (): Handoff => ({
63
81
  ...suffixes(full, offsets),
@@ -98,3 +116,22 @@ export function historyPage(full: Handoff, section: HistorySection, offset: numb
98
116
  const end = Math.min(items.length, offset + limit);
99
117
  return { total: items.length, offset, next: end < items.length ? end : null, items: items.slice(offset, end) };
100
118
  }
119
+
120
+ /** O.10: the optional priority view shares the original total budget. */
121
+ export function boundWorkingHandoff(full: Handoff, budget = DEFAULT_HANDOFF_TOKENS): Handoff {
122
+ const legacy = boundHandoff(full, budget);
123
+ // 40% is a declared allocation hypothesis, not a measured context threshold.
124
+ const view = workingSet(full, budget * .4);
125
+ if (!view) return legacy;
126
+ try { return boundHandoff({ ...full, working_set: view }, budget); }
127
+ catch (error) { if (error instanceof RangeError) return legacy; throw error; }
128
+ }
129
+
130
+ /** R9 uses the same budget; originals are supplied by the ancestry owner. */
131
+ export function boundRecoveryHandoff(full: Handoff, nodes: RecoveryNodes, budget = DEFAULT_HANDOFF_TOKENS, tools?: readonly string[]): Handoff {
132
+ const legacy = boundHandoff(full, budget);
133
+ const view = recoveryWorkingSet(full, nodes, budget * .4, tools);
134
+ if (!view) return legacy;
135
+ try { return boundHandoff({ ...full, working_set: view }, budget); }
136
+ catch (error) { if (error instanceof RangeError) return legacy; throw error; }
137
+ }
@@ -0,0 +1,34 @@
1
+ /** Q.13: observed connectivity is a cache, never authorization or replay evidence. */
2
+ export type IntegrationFailureCode = "offline" | "rate_limited" | "auth_required" | "protocol_error" | "timeout" | "unknown";
3
+ export type IntegrationTime = { at: number; tick: number };
4
+ export type IntegrationHealth = {
5
+ state: "not_checked" | "ready" | "backoff" | "auth_required" | "protocol_error";
6
+ observed: IntegrationTime | null;
7
+ failures: number;
8
+ retryAt: number | null;
9
+ error: IntegrationFailureCode | null;
10
+ };
11
+ // Assumed bounds, not measured optima: avoid tight failure loops and unbounded server delays.
12
+ export const INTEGRATION_BACKOFF_MS = 1_000;
13
+ export const MAX_INTEGRATION_BACKOFF_MS = 60_000;
14
+ export const INTEGRATION_HEALTH_TTL_MS = 30_000;
15
+
16
+ export function initialIntegrationHealth(): IntegrationHealth {
17
+ return { state: "not_checked", observed: null, failures: 0, retryAt: null, error: null };
18
+ }
19
+ export function readyIntegrationHealth(now: IntegrationTime): IntegrationHealth {
20
+ return { ...initialIntegrationHealth(), state: "ready", observed: { ...now } };
21
+ }
22
+ export function failedIntegrationHealth(previous: IntegrationHealth, error: IntegrationFailureCode, now: IntegrationTime, retryAfterMs?: number): IntegrationHealth {
23
+ const failures = Math.min(16, previous.failures + 1);
24
+ const delay = Math.min(MAX_INTEGRATION_BACKOFF_MS, Math.max(INTEGRATION_BACKOFF_MS * 2 ** (failures - 1),
25
+ Number.isFinite(retryAfterMs) ? Math.max(0, retryAfterMs!) : 0));
26
+ const state = error === "auth_required" || error === "protocol_error" ? error : "backoff";
27
+ return { state, observed: { ...now }, failures, retryAt: state === "backoff" ? now.tick + delay : null, error };
28
+ }
29
+ export function integrationHealthView(health: IntegrationHealth, now: IntegrationTime) {
30
+ const age = health.observed === null ? null : Math.max(0, now.tick - health.observed.tick);
31
+ return { state: health.state, observed_at: health.observed?.at ?? null, age_ms: age,
32
+ stale: age === null || age >= INTEGRATION_HEALTH_TTL_MS, error: health.error,
33
+ failures: health.failures, retry_after_ms: health.retryAt === null ? null : Math.max(0, health.retryAt - now.tick) };
34
+ }