@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
@@ -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({
@@ -0,0 +1,206 @@
1
+ /** D11 / O.3: classify durable execution facts, never error words in tool output. */
2
+ import type { LogEvent, ToolResultEvent } from "./events.ts";
3
+ import type { BenchmarkUsage } from "./benchmark.ts";
4
+ import { canonicalValue } from "./continuation.ts";
5
+
6
+ export type ResultClass = "success" | "invalid_input" | "cancelled" | "timeout" | "command_nonzero" |
7
+ "execution_failed" | "rejected" | "unclassified_failure";
8
+
9
+ export function classifyToolResult(event: ToolResultEvent): ResultClass {
10
+ const code = event.diagnostic?.code;
11
+ if (code === "invalid_input") return "invalid_input";
12
+ if (code === "cancelled" || event.outcome?.reason === "cancelled") return "cancelled";
13
+ if (code === "timeout" || event.outcome?.reason === "timeout") return "timeout";
14
+ if (event.effect?.kind === "command" && event.effect.exit !== 0) return "command_nonzero";
15
+ if (event.ok) return "success";
16
+ if (event.diagnostic?.execution === "not_started" ||
17
+ ["version_mismatch", "not_found", "ambiguous_edit", "permission_denied", "environment_missing"].includes(code ?? "")) return "rejected";
18
+ if (code === "execution_failed" || code === "unknown_effect" || event.outcome?.reason === "execution_error") return "execution_failed";
19
+ return "unclassified_failure";
20
+ }
21
+
22
+ export interface DiagnosticRequest {
23
+ request: number;
24
+ node: number | null;
25
+ usage: BenchmarkUsage | null;
26
+ responseModel: string | null;
27
+ failure: string | null;
28
+ firstDeltaMs: number | null;
29
+ wallMs: number | null;
30
+ prefixUnchanged: boolean | null;
31
+ sharedInputBytes: number | null;
32
+ frozenSha256: string | null;
33
+ /** Raw terminal counts can remain known when a cache breakdown is absent. */
34
+ reportedPromptTokens?: number | null;
35
+ reportedOutputTokens?: number | null;
36
+ }
37
+ export interface ToolCounters {
38
+ calls: number; providerCalls: number; invocations: number; nestedInvocations: number;
39
+ results: number; pairedCalls: number; orphanCalls: number; orphanResults: number;
40
+ duplicateProviderCalls: number; duplicateInvocations: number; duplicateResults: number;
41
+ repeatedProviderInputs: number; repeatedInvocationInputs: number;
42
+ classes: Record<ResultClass, number>;
43
+ diagnosticCodes: Record<string, number>;
44
+ commandExits: Record<string, number>;
45
+ expectedNonzeroCommands: number;
46
+ undeclaredNonzeroCommands: number;
47
+ }
48
+ const classes = (): Record<ResultClass, number> => ({ success: 0, invalid_input: 0, cancelled: 0, timeout: 0,
49
+ command_nonzero: 0, execution_failed: 0, rejected: 0, unclassified_failure: 0 });
50
+ const counters = (): ToolCounters => ({ calls: 0, providerCalls: 0, invocations: 0, nestedInvocations: 0,
51
+ results: 0, pairedCalls: 0, orphanCalls: 0, orphanResults: 0, duplicateProviderCalls: 0,
52
+ duplicateInvocations: 0, duplicateResults: 0, repeatedProviderInputs: 0, repeatedInvocationInputs: 0,
53
+ classes: classes(), diagnosticCodes: {}, commandExits: {}, expectedNonzeroCommands: 0, undeclaredNonzeroCommands: 0 });
54
+ function add(record: Record<string, number>, key: string): void { record[key] = (record[key] ?? 0) + 1; }
55
+ const validCount = (value: number): boolean => Number.isSafeInteger(value) && value >= 0;
56
+ const reportedCount = (value: number | null | undefined): number | null => typeof value === "number" && validCount(value) ? value : null;
57
+ const validTime = (value: number | null): number | null => value !== null && Number.isFinite(value) && value >= 0 ? value : null;
58
+ function observedUsage(usage: BenchmarkUsage | null): BenchmarkUsage | null {
59
+ return usage && [usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens,
60
+ usage.cache_creation_input_tokens].every(validCount) ? usage : null;
61
+ }
62
+ function quantile(values: readonly number[], q: number): number | null {
63
+ return values.length ? values.toSorted((a, b) => a - b)[Math.max(0, Math.ceil(values.length * q) - 1)]! : null;
64
+ }
65
+ interface Call {
66
+ name: string; provider: boolean; invocation: boolean; nested: boolean;
67
+ providerInput: string | null; invocationInput: string | null;
68
+ }
69
+
70
+ /**
71
+ * Inputs and outputs remain private. Equal-input counts use canonical original
72
+ * model arguments and immutable admitted-input hashes separately (O.3 / Pi A).
73
+ * Expected nonzero exits need an evaluator declaration; an exit alone cannot
74
+ * prove a command was accidental, and model prose cannot establish intent.
75
+ */
76
+ export function summarizeHarnessDiagnostics(input: {
77
+ events: readonly LogEvent[]; requests: readonly DiagnosticRequest[];
78
+ expectedNonzeroCallIds?: readonly string[];
79
+ }) {
80
+ const tools: Record<string, ToolCounters> = {};
81
+ const tool = (name: string): ToolCounters => tools[name] ??= counters();
82
+ const calls = new Map<string, Call>(), results = new Map<string, ToolResultEvent[]>(), seenEvents = new Set<string>();
83
+ const guidance: Record<string, number> = {}, cuts: Record<string, number> = {}, cleanupProcessExits: Record<string, number> = {};
84
+ let duplicateEvents = 0, interruptions = 0;
85
+ for (const event of input.events) {
86
+ if (seenEvents.has(event.id)) { duplicateEvents++; continue; }
87
+ seenEvents.add(event.id);
88
+ if (event.t === "guidance") add(guidance, event.reason);
89
+ if (event.t === "cut") add(cuts, event.reason);
90
+ if (event.t === "interrupt") interruptions++;
91
+ if (event.t === "process_exit") add(cleanupProcessExits, String(event.exit));
92
+ if (event.t === "assistant") for (const block of event.blocks) {
93
+ if (block.type !== "tool_use") continue;
94
+ const existing = calls.get(block.call_id), stats = tool(block.name);
95
+ if (existing?.provider) stats.duplicateProviderCalls++;
96
+ calls.set(block.call_id, { name: existing?.name ?? block.name, provider: true,
97
+ invocation: existing?.invocation ?? false, nested: existing?.nested ?? false,
98
+ providerInput: canonicalValue(block.input), invocationInput: existing?.invocationInput ?? null });
99
+ }
100
+ if (event.t === "tool_invocation") {
101
+ const existing = calls.get(event.call_id), stats = tool(event.name);
102
+ if (existing?.invocation) stats.duplicateInvocations++;
103
+ calls.set(event.call_id, { name: existing?.name ?? event.name, provider: existing?.provider ?? false,
104
+ invocation: true, nested: event.parent_call_id !== undefined,
105
+ providerInput: existing?.providerInput ?? null, invocationInput: event.input_ref.sha256 });
106
+ }
107
+ if (event.t === "tool_result") {
108
+ const found = results.get(event.call_id) ?? [];
109
+ found.push(event); results.set(event.call_id, found);
110
+ }
111
+ }
112
+ const providerInputs = new Map<string, Set<string>>(), invocationInputs = new Map<string, Set<string>>();
113
+ const repeat = (groups: Map<string, Set<string>>, name: string, key: string | null): boolean => {
114
+ if (key === null) return false;
115
+ const seen = groups.get(name) ?? new Set<string>(), repeated = seen.has(key);
116
+ seen.add(key); groups.set(name, seen); return repeated;
117
+ };
118
+ for (const [id, call] of calls) {
119
+ const stats = tool(call.name); stats.calls++;
120
+ if (call.provider) stats.providerCalls++;
121
+ if (call.invocation) stats.invocations++;
122
+ if (call.nested) stats.nestedInvocations++;
123
+ if (repeat(providerInputs, call.name, call.providerInput)) stats.repeatedProviderInputs++;
124
+ if (repeat(invocationInputs, call.name, call.invocationInput)) stats.repeatedInvocationInputs++;
125
+ if (results.has(id)) stats.pairedCalls++; else stats.orphanCalls++;
126
+ }
127
+ const expected = new Set(input.expectedNonzeroCallIds ?? []);
128
+ for (const [id, found] of results) {
129
+ const call = calls.get(id), stats = tool(call?.name ?? "<orphan>");
130
+ stats.duplicateResults += Math.max(0, found.length - 1);
131
+ for (const event of found) {
132
+ stats.results++;
133
+ if (!call) stats.orphanResults++;
134
+ stats.classes[classifyToolResult(event)]++;
135
+ if (event.diagnostic) add(stats.diagnosticCodes, event.diagnostic.code);
136
+ if (event.effect?.kind === "command") {
137
+ add(stats.commandExits, String(event.effect.exit));
138
+ if (event.effect.exit !== 0) {
139
+ if (expected.has(id)) stats.expectedNonzeroCommands++; else stats.undeclaredNonzeroCommands++;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ const requestIds = new Set<number>(), requests: {
145
+ request: number; node: number | null; usageKnown: boolean; promptTokens: number | null;
146
+ cachedTokens: number | null; cacheWriteTokens: number | null; outputTokens: number | null;
147
+ cacheHitRate: number | null; responseModel: string | null; failure: string | null;
148
+ firstDeltaMs: number | null; wallMs: number | null; prefixUnchanged: boolean | null; sharedInputBytes: number | null;
149
+ }[] = [];
150
+ const models: Record<string, number> = {};
151
+ const usage = { knownRequests: 0, unknownRequests: 0, requestsWithCacheReads: 0, promptTokens: 0, cachedTokens: 0,
152
+ cacheWriteTokens: 0, outputTokens: 0, promptKnownRequests: 0, promptUnknownRequests: 0,
153
+ cacheCoveredPromptTokens: 0, cacheCoverageRate: null as number | null,
154
+ observedCacheHitRate: null as number | null, cacheHitRate: null as number | null };
155
+ const prefix = { sameNodeComparisons: 0, sameNodeMutations: 0, boundaryComparisons: 0, boundaryMutations: 0,
156
+ unknownComparisons: 0, sameNodeFrozenMutations: 0, boundaryFrozenMutations: 0 };
157
+ let previous: DiagnosticRequest | undefined, duplicateRequests = 0;
158
+ for (const request of input.requests.toSorted((a, b) => a.request - b.request)) {
159
+ if (requestIds.has(request.request)) { duplicateRequests++; continue; }
160
+ requestIds.add(request.request);
161
+ const observed = observedUsage(request.usage);
162
+ const prompt = reportedCount(request.reportedPromptTokens) ?? (observed ?
163
+ observed.input_tokens + observed.cache_read_input_tokens + observed.cache_creation_input_tokens : null);
164
+ const output = reportedCount(request.reportedOutputTokens) ?? observed?.output_tokens ?? null;
165
+ if (prompt !== null) { usage.promptKnownRequests++; usage.promptTokens += prompt; } else usage.promptUnknownRequests++;
166
+ if (output !== null) usage.outputTokens += output;
167
+ if (observed) {
168
+ usage.knownRequests++; usage.cacheCoveredPromptTokens += prompt!; usage.cachedTokens += observed.cache_read_input_tokens;
169
+ usage.cacheWriteTokens += observed.cache_creation_input_tokens;
170
+ if (observed.cache_read_input_tokens > 0) usage.requestsWithCacheReads++;
171
+ } else usage.unknownRequests++;
172
+ if (request.responseModel !== null) add(models, request.responseModel);
173
+ if (previous) {
174
+ if (previous.node === null || request.node === null || request.prefixUnchanged === null) prefix.unknownComparisons++;
175
+ else if (previous.node === request.node) { prefix.sameNodeComparisons++; if (!request.prefixUnchanged) prefix.sameNodeMutations++; }
176
+ else { prefix.boundaryComparisons++; if (!request.prefixUnchanged) prefix.boundaryMutations++; }
177
+ if (previous.frozenSha256 !== null && request.frozenSha256 !== null && previous.frozenSha256 !== request.frozenSha256) {
178
+ if (previous.node === request.node && request.node !== null) prefix.sameNodeFrozenMutations++;
179
+ else if (previous.node !== null && request.node !== null) prefix.boundaryFrozenMutations++;
180
+ }
181
+ }
182
+ previous = request;
183
+ requests.push({ request: request.request, node: request.node, usageKnown: observed !== null, promptTokens: prompt,
184
+ cachedTokens: observed?.cache_read_input_tokens ?? null, cacheWriteTokens: observed?.cache_creation_input_tokens ?? null,
185
+ outputTokens: output, cacheHitRate: observed && prompt ? observed.cache_read_input_tokens / prompt : null,
186
+ responseModel: request.responseModel, failure: request.failure, firstDeltaMs: validTime(request.firstDeltaMs),
187
+ wallMs: validTime(request.wallMs), prefixUnchanged: request.prefixUnchanged, sharedInputBytes: request.sharedInputBytes });
188
+ }
189
+ usage.observedCacheHitRate = usage.cacheCoveredPromptTokens > 0 ? usage.cachedTokens / usage.cacheCoveredPromptTokens : null;
190
+ usage.cacheCoverageRate = usage.promptUnknownRequests === 0 && usage.promptTokens > 0 ? usage.cacheCoveredPromptTokens / usage.promptTokens : null;
191
+ usage.cacheHitRate = usage.unknownRequests === 0 && usage.promptTokens > 0 ? usage.cachedTokens / usage.promptTokens : null;
192
+ const delta = requests.flatMap(r => r.firstDeltaMs === null ? [] : [r.firstDeltaMs]);
193
+ const totals = counters();
194
+ for (const stats of Object.values(tools)) {
195
+ for (const key of Object.keys(totals) as (keyof ToolCounters)[]) {
196
+ const value = stats[key];
197
+ if (typeof value === "number") (totals[key] as number) += value;
198
+ }
199
+ for (const key of Object.keys(stats.classes) as ResultClass[]) totals.classes[key] += stats.classes[key];
200
+ for (const key of Object.keys(stats.commandExits)) totals.commandExits[key] = (totals.commandExits[key] ?? 0) + stats.commandExits[key]!;
201
+ for (const key of Object.keys(stats.diagnosticCodes)) totals.diagnosticCodes[key] = (totals.diagnosticCodes[key] ?? 0) + stats.diagnosticCodes[key]!;
202
+ }
203
+ return { eventCount: seenEvents.size, duplicateEvents, tools, totals, guidance, cuts, interruptions, cleanupProcessExits,
204
+ requestCount: requests.length, duplicateRequests, models, usage, prefix, requests,
205
+ firstDeltaMs: { samples: delta.length, missing: requests.length - delta.length, p50: quantile(delta, 0.5), p95: quantile(delta, 0.95) } };
206
+ }
@@ -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
+ }
@@ -3,8 +3,8 @@
3
3
  * parses, reports conflicts by name, and lets the last definition win.
4
4
  *
5
5
  * Silent drop of one side of a conflict is how a shortcut "just stops
6
- * working" with no trail (feat-theming design). Namespaced ids follow
7
- * pi's scheme; the action list is ours.
6
+ * working" with no trail (feat-theming design). The action list is
7
+ * xeno's (core/action-defaults.ts, D20).
8
8
  */
9
9
 
10
10
  import { ACTION_DEFAULTS, ACTION_ALIASES } from "./action-defaults.ts";
@@ -88,26 +88,14 @@ export function resolveAction(map: Keymap, spec: string): ActionId | undefined {
88
88
  }
89
89
 
90
90
  export function defaultKeymap(platform: "posix" | "win32" | "wsl" = "posix"): Keymap {
91
- // Pi's Windows shortcuts avoid terminal-reserved chords; WSL still supports job control.
92
- return parseKeymap({ ...ACTION_DEFAULTS, ...(platform === "posix" ? {} : {
93
- "app.suspend": platform === "win32" ? [] : ["ctrl+z"],
94
- "editor.undo": platform === "win32" ? ["ctrl+z"] : ["alt+z"],
95
- "app.cycleModelReverse": ["alt+p"], "app.message.followUp": ["ctrl+q"],
96
- "app.message.dequeue": ["alt+q"], "app.clipboard.pasteImage": ["alt+v"],
97
- "tui.altScreen.previousPrompt": ["ctrl+up"], "tui.altScreen.nextPrompt": ["ctrl+down"],
98
- "tui.altScreen.search": ["ctrl+f"],
99
- }) });
91
+ // Windows has no job control; Ctrl+Z stays a plain key there.
92
+ return parseKeymap({ ...ACTION_DEFAULTS, ...(platform === "win32" ? { "app.suspend": [] } : {}) });
100
93
  }
101
94
 
102
- /** Modal scopes deliberately share chords. Only collisions within one owner replace a binding. */
95
+ /** Scopes share chords on purpose: arrows mean history in the composer and movement in a list. */
103
96
  export function actionScope(action: string): string {
104
- if (action === "app.exit") return "exit";
105
- if (action === "tui.input.copy") return "copy";
106
- if (action.startsWith("tui.select.")) return "select";
107
- if (action.startsWith("tui.altScreen.")) return "viewport";
108
- if (action.startsWith("app.tree.")) return "tree";
109
- if (action.startsWith("app.models.")) return "models";
110
- if (action.startsWith("app.session.") && !["app.session.new", "app.session.tree", "app.session.fork", "app.session.resume"].includes(action)) return "session";
97
+ if (action.startsWith("editor.")) return "editor";
98
+ if (action.startsWith("list.")) return "list";
111
99
  return "global";
112
100
  }
113
101
  export function parseKeymap(json: unknown, base?: Keymap): Keymap {
@@ -135,7 +123,7 @@ export function parseKeymap(json: unknown, base?: Keymap): Keymap {
135
123
  }
136
124
  }
137
125
  const byKey: Record<string,ActionId> = {};
138
- for (const [action,keys] of Object.entries(byAction)) if (["global", "exit"].includes(actionScope(action))) for (const key of keys) byKey[key] = action as ActionId;
126
+ for (const [action,keys] of Object.entries(byAction)) if (actionScope(action) === "global") for (const key of keys) byKey[key] = action as ActionId;
139
127
  return { byKey, byAction, diagnostics };
140
128
  }
141
129