@namzu/sdk 39.0.0 → 40.0.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 (183) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  3. package/dist/connector/mcp/adapter.js +20 -6
  4. package/dist/connector/mcp/adapter.js.map +1 -1
  5. package/dist/manager/run/persistence.d.ts +8 -0
  6. package/dist/manager/run/persistence.d.ts.map +1 -1
  7. package/dist/manager/run/persistence.js +12 -0
  8. package/dist/manager/run/persistence.js.map +1 -1
  9. package/dist/public-runtime.d.ts +3 -1
  10. package/dist/public-runtime.d.ts.map +1 -1
  11. package/dist/public-runtime.js +5 -1
  12. package/dist/public-runtime.js.map +1 -1
  13. package/dist/public-tools.d.ts +11 -0
  14. package/dist/public-tools.d.ts.map +1 -1
  15. package/dist/public-tools.js +14 -0
  16. package/dist/public-tools.js.map +1 -1
  17. package/dist/registry/tool/execute.d.ts.map +1 -1
  18. package/dist/registry/tool/execute.js +2 -3
  19. package/dist/registry/tool/execute.js.map +1 -1
  20. package/dist/registry/tool/portable.d.ts +65 -0
  21. package/dist/registry/tool/portable.d.ts.map +1 -0
  22. package/dist/registry/tool/portable.js +244 -0
  23. package/dist/registry/tool/portable.js.map +1 -0
  24. package/dist/registry/tool/schema.d.ts +32 -5
  25. package/dist/registry/tool/schema.d.ts.map +1 -1
  26. package/dist/registry/tool/schema.js +35 -9
  27. package/dist/registry/tool/schema.js.map +1 -1
  28. package/dist/registry/toolset/catalog.js +8 -8
  29. package/dist/registry/toolset/catalog.js.map +1 -1
  30. package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
  31. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
  32. package/dist/runtime/jobs/awaited-jobs.js +259 -0
  33. package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
  34. package/dist/runtime/jobs/registry.d.ts +33 -2
  35. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  36. package/dist/runtime/jobs/registry.js +37 -0
  37. package/dist/runtime/jobs/registry.js.map +1 -1
  38. package/dist/runtime/query/executor.d.ts +28 -0
  39. package/dist/runtime/query/executor.d.ts.map +1 -1
  40. package/dist/runtime/query/executor.js +39 -1
  41. package/dist/runtime/query/executor.js.map +1 -1
  42. package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
  43. package/dist/runtime/query/file-evidence-context.js +159 -43
  44. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  45. package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
  46. package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
  47. package/dist/runtime/query/file-evidence-replay.js +647 -0
  48. package/dist/runtime/query/file-evidence-replay.js.map +1 -0
  49. package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
  50. package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
  51. package/dist/runtime/query/file-evidence-seed.js +100 -0
  52. package/dist/runtime/query/file-evidence-seed.js.map +1 -0
  53. package/dist/runtime/query/index.d.ts.map +1 -1
  54. package/dist/runtime/query/index.js +94 -2
  55. package/dist/runtime/query/index.js.map +1 -1
  56. package/dist/runtime/query/iteration/index.d.ts +87 -9
  57. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  58. package/dist/runtime/query/iteration/index.js +193 -28
  59. package/dist/runtime/query/iteration/index.js.map +1 -1
  60. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  61. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  62. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  63. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  64. package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
  65. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  66. package/dist/runtime/query/plugin-hooks.d.ts +14 -0
  67. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  68. package/dist/runtime/query/plugin-hooks.js +18 -0
  69. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  70. package/dist/runtime/query/repeat-call.d.ts +17 -4
  71. package/dist/runtime/query/repeat-call.d.ts.map +1 -1
  72. package/dist/runtime/query/repeat-call.js +26 -19
  73. package/dist/runtime/query/repeat-call.js.map +1 -1
  74. package/dist/runtime/query/steering.d.ts +11 -1
  75. package/dist/runtime/query/steering.d.ts.map +1 -1
  76. package/dist/runtime/query/steering.js +12 -1
  77. package/dist/runtime/query/steering.js.map +1 -1
  78. package/dist/runtime/query/tooling.d.ts +2 -0
  79. package/dist/runtime/query/tooling.d.ts.map +1 -1
  80. package/dist/runtime/query/tooling.js +1 -0
  81. package/dist/runtime/query/tooling.js.map +1 -1
  82. package/dist/scheduler/completion-inbox.d.ts +48 -2
  83. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  84. package/dist/scheduler/completion-inbox.js +102 -10
  85. package/dist/scheduler/completion-inbox.js.map +1 -1
  86. package/dist/tools/builtins/bash.d.ts.map +1 -1
  87. package/dist/tools/builtins/bash.js +4 -10
  88. package/dist/tools/builtins/bash.js.map +1 -1
  89. package/dist/tools/builtins/edit-apply.d.ts +126 -0
  90. package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
  91. package/dist/tools/builtins/edit-apply.js +360 -0
  92. package/dist/tools/builtins/edit-apply.js.map +1 -0
  93. package/dist/tools/builtins/edit.d.ts +143 -1
  94. package/dist/tools/builtins/edit.d.ts.map +1 -1
  95. package/dist/tools/builtins/edit.js +37 -219
  96. package/dist/tools/builtins/edit.js.map +1 -1
  97. package/dist/tools/builtins/index.d.ts +1 -0
  98. package/dist/tools/builtins/index.d.ts.map +1 -1
  99. package/dist/tools/builtins/index.js +9 -3
  100. package/dist/tools/builtins/index.js.map +1 -1
  101. package/dist/tools/builtins/job.js +1 -1
  102. package/dist/tools/builtins/job.js.map +1 -1
  103. package/dist/tools/builtins/read-file.d.ts +2 -2
  104. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  105. package/dist/tools/builtins/read-file.js +50 -65
  106. package/dist/tools/builtins/read-file.js.map +1 -1
  107. package/dist/tools/builtins/read-render.d.ts +56 -0
  108. package/dist/tools/builtins/read-render.d.ts.map +1 -0
  109. package/dist/tools/builtins/read-render.js +73 -0
  110. package/dist/tools/builtins/read-render.js.map +1 -0
  111. package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
  112. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
  113. package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
  114. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
  115. package/dist/tools/builtins/wait-for-job.d.ts +6 -0
  116. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
  117. package/dist/tools/builtins/wait-for-job.js +162 -0
  118. package/dist/tools/builtins/wait-for-job.js.map +1 -0
  119. package/dist/tools/builtins/write-file.js +5 -0
  120. package/dist/tools/builtins/write-file.js.map +1 -1
  121. package/dist/tools/coordinator/index.d.ts.map +1 -1
  122. package/dist/tools/coordinator/index.js +1 -7
  123. package/dist/tools/coordinator/index.js.map +1 -1
  124. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  125. package/dist/tools/file-read-tracker.js +88 -10
  126. package/dist/tools/file-read-tracker.js.map +1 -1
  127. package/dist/types/message/index.d.ts +1 -1
  128. package/dist/types/message/index.d.ts.map +1 -1
  129. package/dist/types/message/index.js +2 -0
  130. package/dist/types/message/index.js.map +1 -1
  131. package/dist/types/run/entity.d.ts +13 -0
  132. package/dist/types/run/entity.d.ts.map +1 -1
  133. package/dist/types/sandbox/index.d.ts +15 -14
  134. package/dist/types/sandbox/index.d.ts.map +1 -1
  135. package/dist/types/sandbox/index.js.map +1 -1
  136. package/dist/types/tool/index.d.ts +109 -0
  137. package/dist/types/tool/index.d.ts.map +1 -1
  138. package/dist/types/tool/index.js.map +1 -1
  139. package/dist/utils/env.d.ts +19 -0
  140. package/dist/utils/env.d.ts.map +1 -0
  141. package/dist/utils/env.js +25 -0
  142. package/dist/utils/env.js.map +1 -0
  143. package/package.json +1 -1
  144. package/src/connector/mcp/adapter.ts +20 -6
  145. package/src/manager/run/persistence.ts +12 -0
  146. package/src/public-runtime.ts +9 -1
  147. package/src/public-tools.ts +18 -0
  148. package/src/registry/tool/execute.ts +2 -4
  149. package/src/registry/tool/portable.ts +264 -0
  150. package/src/registry/tool/schema.ts +38 -8
  151. package/src/registry/toolset/catalog.ts +8 -9
  152. package/src/runtime/jobs/awaited-jobs.ts +271 -0
  153. package/src/runtime/jobs/registry.ts +50 -0
  154. package/src/runtime/query/executor.ts +49 -1
  155. package/src/runtime/query/file-evidence-context.ts +190 -46
  156. package/src/runtime/query/file-evidence-replay.ts +776 -0
  157. package/src/runtime/query/file-evidence-seed.ts +126 -0
  158. package/src/runtime/query/index.ts +104 -2
  159. package/src/runtime/query/iteration/index.ts +202 -28
  160. package/src/runtime/query/iteration/phases/context.ts +10 -0
  161. package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
  162. package/src/runtime/query/plugin-hooks.ts +20 -0
  163. package/src/runtime/query/repeat-call.ts +28 -18
  164. package/src/runtime/query/steering.ts +11 -0
  165. package/src/runtime/query/tooling.ts +3 -0
  166. package/src/scheduler/completion-inbox.ts +105 -9
  167. package/src/tools/builtins/bash.ts +4 -10
  168. package/src/tools/builtins/edit-apply.ts +456 -0
  169. package/src/tools/builtins/edit.ts +39 -270
  170. package/src/tools/builtins/index.ts +9 -3
  171. package/src/tools/builtins/job.ts +1 -1
  172. package/src/tools/builtins/read-file.ts +56 -77
  173. package/src/tools/builtins/read-render.ts +104 -0
  174. package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
  175. package/src/tools/builtins/wait-for-job.ts +184 -0
  176. package/src/tools/builtins/write-file.ts +5 -0
  177. package/src/tools/coordinator/index.ts +1 -7
  178. package/src/tools/file-read-tracker.ts +85 -7
  179. package/src/types/message/index.ts +2 -0
  180. package/src/types/run/entity.ts +14 -0
  181. package/src/types/sandbox/index.ts +15 -14
  182. package/src/types/tool/index.ts +104 -0
  183. package/src/utils/env.ts +23 -0
@@ -0,0 +1,776 @@
1
+ import { resolve } from 'node:path'
2
+ import { isClearedToolResult } from '../../compaction/tool-result-editing.js'
3
+ import { replayEditCallWithin } from '../../tools/builtins/edit-apply.js'
4
+ import { EditTool } from '../../tools/builtins/edit.js'
5
+ import { ReadFileTool } from '../../tools/builtins/read-file.js'
6
+ import {
7
+ type ReadWindowRequest,
8
+ renderNumberedRead,
9
+ resolveReadWindow,
10
+ } from '../../tools/builtins/read-render.js'
11
+ import { WriteFileTool } from '../../tools/builtins/write-file.js'
12
+ import type { Message, ToolCall } from '../../types/message/index.js'
13
+ import type { FileReadTracker } from '../../types/tool/index.js'
14
+ import { isSkippedToolResult } from './plugin-hooks.js'
15
+
16
+ /**
17
+ * Reconstructing a file's body from calls the conversation can still see.
18
+ *
19
+ * One module because two callers need the same answer and must not each have
20
+ * their own idea of it: the derived work context asks "is this ledger entry's
21
+ * body one the model can rebuild from what is in front of it?", and the resume
22
+ * seed asks "which of these ledger entries can be rebuilt at all?". A
23
+ * predicate that admitted a hop in one and refused it in the other would mean
24
+ * a path referenced as evidence that the ledger never witnessed, or the
25
+ * reverse. Everything below is a function of the messages, the ledger and the
26
+ * bounds — there is no filesystem access anywhere in this file.
27
+ */
28
+
29
+ /**
30
+ * Edit CALLS one chain may carry before the path is withheld.
31
+ *
32
+ * On calls rather than replacements because the bound is on replay work, and
33
+ * an `edits: [...]` batch is one pass over the content however many hunks it
34
+ * holds. Eight is generous for the shape this exists for — a file written and
35
+ * then refined within a turn — and short enough that the whole request's
36
+ * replay stays a rounding error beside the model call it rides on.
37
+ */
38
+ export const MAX_EDIT_CALLS = 8
39
+
40
+ /**
41
+ * Content a replay may materialise across every path in one pass.
42
+ *
43
+ * Counted cumulatively in UTF-16 units — the measure the caps below and the
44
+ * step-context budget already use — and charged for the write body each chain
45
+ * starts from as well as every body an edit builds on top of it, because those
46
+ * are the strings actually built. Each operation is measured exactly against
47
+ * the content it is about to be applied to and refused before it is built, so
48
+ * a pass whose history is full of long chains it is going to refuse anyway
49
+ * cannot make itself slow proving that. A refusal is charged nothing: the body
50
+ * was never built, and the paths after it keep their room.
51
+ */
52
+ export const MAX_REPLAYED_UNITS = 256 * 1024
53
+
54
+ /**
55
+ * Arguments longer than a real tool call carries; past this the call is not
56
+ * read AS EVIDENCE.
57
+ *
58
+ * A bound on what may be BELIEVED, and on nothing else. The path such a call
59
+ * declares is still read, under no bound at all — see {@link declaredPath} —
60
+ * because a write too long to quote back is still a write that happened, and a
61
+ * pass that could not say WHICH file it happened to would have to abandon every
62
+ * other path in the conversation to stay honest.
63
+ */
64
+ const MAX_ARGUMENT_UNITS = 32_000
65
+
66
+ /** Call ids longer than any provider mints. */
67
+ const MAX_CALL_ID_UNITS = 256
68
+
69
+ /**
70
+ * Path lengths a `write` entry may go out with.
71
+ *
72
+ * Exported because `file-evidence-context.ts` puts the same bound on the
73
+ * spelling a `read` entry emits — a read is the other thing that puts a path
74
+ * in the work-context message, and the constant belongs to this module.
75
+ */
76
+ export const MAX_PATH_UNITS = 512
77
+
78
+ /**
79
+ * Arguments this module will not read as JSON at all, at any bound.
80
+ *
81
+ * {@link MAX_ARGUMENT_UNITS} governs what may be BELIEVED about a call, and
82
+ * deliberately leaves attribution unbounded so that one large `write` cannot
83
+ * cost a conversation every other witness in it. Unbounded is still not free:
84
+ * `JSON.parse` over a multi-megabyte body is real work, and it was being done
85
+ * up to three times for one call — once to collect the paths, once to key the
86
+ * mutation, once to read the call as evidence. Two of those are gone: the
87
+ * attribution answer is now memoised for the whole pass (see
88
+ * {@link PathAttributions}), and the evidence read never reaches a call past
89
+ * the far smaller bound above. What is left is this ceiling on the single
90
+ * remaining parse.
91
+ *
92
+ * About a megabyte, some thirty times the evidence bound, because the rule is
93
+ * that an oversize-but-ORDINARY write stays attributable — a generated file, a
94
+ * bundled config, a long document — while the pathological input is refused. A
95
+ * call past it is attributable to nothing, and the walk treats it exactly as
96
+ * it treats one that names no `path`: there is no key to withdraw, so the
97
+ * whole pass is abandoned and the conversation keeps the empty ledger a resume
98
+ * has always started from.
99
+ */
100
+ const MAX_ATTRIBUTION_UNITS = 1024 * 1024
101
+
102
+ /**
103
+ * One pass's answer to "which file did this call touch", memoised.
104
+ *
105
+ * Keyed on the call OBJECT rather than on its id: an id claimed by two calls
106
+ * is an ambiguity this module refuses to settle by position, and settling it
107
+ * by cache hit instead would be the same mistake wearing a different hat. A
108
+ * `WeakMap` because the entries are worth exactly as long as the transcript
109
+ * they describe, and the value is the declared path or `null` for a call that
110
+ * declares none — `undefined` means only "not asked yet".
111
+ */
112
+ export type PathAttributions = WeakMap<ToolCall, string | null>
113
+
114
+ export function createPathAttributions(): PathAttributions {
115
+ return new WeakMap()
116
+ }
117
+
118
+ /**
119
+ * A call's arguments as JSON, or `undefined` for arguments this module will
120
+ * not read.
121
+ *
122
+ * Every `JSON.parse` of a tool call in this file goes through here, so the
123
+ * ceiling is one rule rather than one rule per reader, and malformed input is
124
+ * a value the callers test rather than an exception they have to be wrapped
125
+ * against.
126
+ */
127
+ function parseArguments(call: ToolCall): unknown {
128
+ if (call.function.arguments.length > MAX_ATTRIBUTION_UNITS) return undefined
129
+ try {
130
+ return JSON.parse(call.function.arguments)
131
+ } catch {
132
+ return undefined
133
+ }
134
+ }
135
+
136
+ /**
137
+ * The ledger key a tool-call path belongs to, or `undefined` for a path this
138
+ * pass may not key at all.
139
+ *
140
+ * A resolver rather than a working directory, because the two callers do not
141
+ * key alike and must not be made to. The projection READS a ledger the tools
142
+ * wrote and looks entries up lexically, as it always has: a spelling that does
143
+ * not match the tool's canonical key simply finds nothing. A seed WRITES that
144
+ * ledger, so its keys have to be the ones the tools will come looking for —
145
+ * which means resolving a path the way `write` and `edit` resolve it, through
146
+ * every symlink, and that is filesystem work nothing in this module is allowed
147
+ * to do. So the caller resolves first and hands the answers in.
148
+ */
149
+ export type FileKeyResolver = (path: string) => string | undefined
150
+
151
+ /**
152
+ * The lexical key space: the path resolved against the working directory, or
153
+ * the path as written when the tools address a sandbox.
154
+ *
155
+ * What a reader of the ledger uses. Not what a writer of it may use: see
156
+ * {@link FileKeyResolver}.
157
+ */
158
+ export function lexicalFileKeys(workingDirectory: string, sandboxed: boolean): FileKeyResolver {
159
+ return (path: string) => (sandboxed ? path : resolve(workingDirectory, path))
160
+ }
161
+
162
+ /** The calls and receipts of one conversation, indexed by call id. */
163
+ export interface VisibleHistory {
164
+ /** `null` where an id was claimed by more than one assistant call. */
165
+ readonly calls: ReadonlyMap<string, ToolCall | null>
166
+ /** `null` where an id was answered more than once. */
167
+ readonly results: ReadonlyMap<string, Message | null>
168
+ /** The ledger key a tool-call path resolves to. */
169
+ readonly keyOf: FileKeyResolver
170
+ }
171
+
172
+ /**
173
+ * Index a transcript once, so every lookup below is a map hit.
174
+ *
175
+ * An id claimed twice, or answered twice, indexes to `null` rather than to
176
+ * whichever message came last: two calls wearing one id is an ambiguity, and
177
+ * resolving it by position would let the second silently vouch for the first.
178
+ */
179
+ export function indexVisibleHistory(
180
+ messages: readonly Message[],
181
+ keyOf: FileKeyResolver,
182
+ ): VisibleHistory {
183
+ const calls = new Map<string, ToolCall | null>()
184
+ const results = new Map<string, Message | null>()
185
+ for (const message of messages) {
186
+ if (message.role === 'assistant') {
187
+ for (const call of message.toolCalls ?? []) {
188
+ calls.set(call.id, calls.has(call.id) ? null : call)
189
+ }
190
+ } else if (message.role === 'tool') {
191
+ results.set(message.toolCallId, results.has(message.toolCallId) ? null : message)
192
+ }
193
+ }
194
+ return { calls, results, keyOf }
195
+ }
196
+
197
+ /**
198
+ * Every path a replay of this history might have to key, in the order the
199
+ * calls arrived.
200
+ *
201
+ * For a caller that has to resolve them before the replay can run, and only
202
+ * for that: nothing here decides anything about a path. Empty when the history
203
+ * holds no `write` and no `edit` call at all, because a claim is only ever made
204
+ * by a mutation — a conversation that only read files reconstructs nothing, and
205
+ * resolving its reads would be filesystem work with no possible result.
206
+ *
207
+ * Every mutation the walk can ATTRIBUTE has to appear here, the ones it will
208
+ * refuse to reconstruct included. This used to drop a call whose arguments ran
209
+ * past the evidence bound, so the walk reached it holding no key for its path,
210
+ * read that as a mutation it could not attribute, and abandoned the seeding
211
+ * entirely: one thirty-kilobyte `write` anywhere in a conversation erased every
212
+ * witness in it. The bounds belong to what may be believed; the path is read
213
+ * here out of the same field the walk reads it from, under no bound at all.
214
+ */
215
+ export function collectObservedPaths(
216
+ messages: readonly Message[],
217
+ attributions: PathAttributions = createPathAttributions(),
218
+ ): readonly string[] {
219
+ const paths = new Set<string>()
220
+ let mutated = false
221
+ for (const message of messages) {
222
+ if (message.role !== 'assistant') continue
223
+ for (const call of message.toolCalls ?? []) {
224
+ const name = call.function.name
225
+ if (name !== 'write' && name !== 'edit' && name !== 'read') continue
226
+ if (name !== 'read') mutated = true
227
+ const path = declaredPath(call, attributions)
228
+ if (path !== undefined) paths.add(path)
229
+ }
230
+ }
231
+ return mutated ? [...paths] : []
232
+ }
233
+
234
+ /**
235
+ * The `path` argument a tool call declares, whatever else its input holds.
236
+ *
237
+ * The one attribution rule, used both to collect the paths a pass must resolve
238
+ * and to key each mutation as the walk reaches it — one function, so the two
239
+ * can never disagree about which file a call touched, which is the disagreement
240
+ * that turns an ordinary large write into a total loss.
241
+ *
242
+ * The recorded arguments and nothing else. A receipt names the path too, but it
243
+ * names it inside a sentence a path may itself contain, in a spelling that
244
+ * differs between the host and sandbox branches, and compaction clears receipts
245
+ * while it never clears a call. A call the provider stream cut off mid-JSON
246
+ * carries `{}` here: `function.arguments` is normalized to that and the raw
247
+ * buffer moves to `metadata.partialArguments`, which is what the model was
248
+ * saying rather than what ran — a `repairToolCall` hook may have rewritten the
249
+ * arguments before execution. So a truncated call is attributable to nothing,
250
+ * and the walk treats it as such. So is a call whose arguments run past
251
+ * {@link MAX_ATTRIBUTION_UNITS}, which this does not read at all.
252
+ *
253
+ * Answered once per pass and remembered. The path collection and the walk ask
254
+ * the same question of the same calls, and a `write` carries a whole file body
255
+ * in the string being parsed to answer it.
256
+ */
257
+ function declaredPath(call: ToolCall, attributions: PathAttributions): string | undefined {
258
+ const remembered = attributions.get(call)
259
+ if (remembered !== undefined) return remembered ?? undefined
260
+ const parsed = parseArguments(call)
261
+ const declared =
262
+ typeof parsed === 'object' && parsed !== null ? (parsed as { path?: unknown }).path : undefined
263
+ const path = typeof declared === 'string' && declared.length > 0 ? declared : null
264
+ attributions.set(call, path)
265
+ return path ?? undefined
266
+ }
267
+
268
+ /**
269
+ * The visibility every hop must pass, write or edit, root or tip.
270
+ *
271
+ * One function because a middle hop is not a lesser claim than the last one.
272
+ * A chain whose second edit was cleared by compaction, or came back an error,
273
+ * or arrived truncated, reconstructs nothing at all — so the whole path is
274
+ * withheld rather than emitted as the part still visible, which would name a
275
+ * body the model cannot rebuild.
276
+ *
277
+ * A call a `pre_tool_use` hook SKIPPED is the one refusal that does not arrive
278
+ * as an error. The hook declined the call; nothing failed, so the receipt is a
279
+ * plain success carrying the sentence `plugin-hooks.ts` writes for it, and
280
+ * reading that as a `write` would hand back a body the tool was never allowed
281
+ * to put on disk — a fingerprint for a file that still holds whatever it held
282
+ * before, and a spurious drift refusal on the next edit. Recognised through
283
+ * the same function that writes the sentence, so the two cannot drift apart.
284
+ */
285
+ export function visibleCall(
286
+ history: VisibleHistory,
287
+ id: string,
288
+ name: 'write' | 'edit' | 'read',
289
+ ): ToolCall | undefined {
290
+ const call = history.calls.get(id)
291
+ const receipt = history.results.get(id)
292
+ if (
293
+ !call ||
294
+ call.function.name !== name ||
295
+ call.metadata?.inputTruncated ||
296
+ call.function.arguments.length > MAX_ARGUMENT_UNITS ||
297
+ id.length > MAX_CALL_ID_UNITS ||
298
+ !receipt ||
299
+ receipt.role !== 'tool' ||
300
+ receipt.isError ||
301
+ typeof receipt.content !== 'string' ||
302
+ isClearedToolResult(receipt.content) ||
303
+ isSkippedToolResult(name, receipt.content)
304
+ )
305
+ return undefined
306
+ return call
307
+ }
308
+
309
+ /** A full body that arrived whole in one visible call. */
310
+ export function visibleWrite(
311
+ history: VisibleHistory,
312
+ id: string,
313
+ ): { readonly path: string; readonly key: string; readonly body: string } | undefined {
314
+ const call = visibleCall(history, id, 'write')
315
+ if (!call) return
316
+ const input = WriteFileTool.inputSchema.safeParse(parseArguments(call))
317
+ if (!input.success || input.data.path.length > MAX_PATH_UNITS) return
318
+ const body = input.data.content ?? input.data.newStr
319
+ if (typeof body !== 'string') return
320
+ const key = history.keyOf(input.data.path)
321
+ if (key === undefined) return
322
+ return { path: input.data.path, key, body }
323
+ }
324
+
325
+ /**
326
+ * One visible edit call's arguments, ready to replay.
327
+ *
328
+ * No length bound on the path here, unlike the write above. A write's path is
329
+ * the spelling an entry goes out with; an edit is held to the KEY it resolves
330
+ * to, which its chain's root has already been bounded on.
331
+ */
332
+ export function visibleEdit(
333
+ history: VisibleHistory,
334
+ id: string,
335
+ ): { readonly key: string; readonly input: unknown } | undefined {
336
+ const call = visibleCall(history, id, 'edit')
337
+ if (!call) return
338
+ const input = EditTool.inputSchema.safeParse(parseArguments(call))
339
+ if (!input.success) return
340
+ const key = history.keyOf(input.data.path)
341
+ if (key === undefined) return
342
+ return { key, input: input.data }
343
+ }
344
+
345
+ /** Room left for content this pass may still materialise. */
346
+ export interface ReplayBudget {
347
+ remaining: number
348
+ }
349
+
350
+ export function createReplayBudget(units: number = MAX_REPLAYED_UNITS): ReplayBudget {
351
+ return { remaining: units }
352
+ }
353
+
354
+ /**
355
+ * Charge one body against the pass's replay allowance.
356
+ *
357
+ * Checked before it is taken, so a refusal costs nothing. Deducting first and
358
+ * reporting afterwards left the allowance negative, and one oversized path
359
+ * then refused every admissible path behind it in the same pass.
360
+ */
361
+ export function spend(budget: ReplayBudget, units: number): boolean {
362
+ if (units > budget.remaining) return false
363
+ budget.remaining -= units
364
+ return true
365
+ }
366
+
367
+ /**
368
+ * Replay one edit call onto the body in hand, within what the budget has left.
369
+ *
370
+ * `undefined` for a hop the budget turns away and for one that no longer
371
+ * applies, because both mean the same thing to every caller here: this path
372
+ * reconstructs nothing. The charge is settled either way — what the attempt
373
+ * built, it built — and for a hop refused at its first operation that charge
374
+ * is zero.
375
+ */
376
+ export function replayHop(
377
+ content: string,
378
+ input: unknown,
379
+ budget: ReplayBudget,
380
+ ): string | undefined {
381
+ const replay = replayEditCallWithin(content, input, budget.remaining)
382
+ budget.remaining = Math.max(budget.remaining - replay.charged, 0)
383
+ return replay.outcome === 'replayed' ? replay.content : undefined
384
+ }
385
+
386
+ /**
387
+ * Whether a refused mutation has reported this path stale since the ledger
388
+ * last observed it.
389
+ *
390
+ * That refusal read the real file to make its comparison, so the ledger knows
391
+ * its body is behind disk without a consumer here touching the filesystem.
392
+ * The reconstruction would still be a body the model can rebuild — but not the
393
+ * FILE's body, which is what an entry claims. Withheld until a real
394
+ * observation re-baselines the ledger, which is also what clears the flag.
395
+ */
396
+ export function knownStale(tracker: FileReadTracker, key: string): boolean {
397
+ return tracker.driftObserved?.(key) === true
398
+ }
399
+
400
+ /**
401
+ * Paths one pass may hold a reconstructed body for at a time.
402
+ *
403
+ * The projection emits at most this many entries and keeps the most recent, so
404
+ * a pass that tracked more would be doing work whose result is discarded.
405
+ */
406
+ export const MAX_WITNESSED_PATHS = 6
407
+
408
+ /** What one pass of {@link replayObservationLedger} established. */
409
+ export interface LedgerReplayReport {
410
+ /** Paths whose body was reconstructed exactly, with a fingerprint and a witness. */
411
+ readonly pathsWitnessed: number
412
+ /**
413
+ * Paths the conversation demonstrably mutated but whose body could not be
414
+ * rebuilt. Nothing is written to the ledger for these; see {@link commit}.
415
+ */
416
+ readonly pathsSeen: number
417
+ /**
418
+ * Content materialised, in UTF-16 units. Never above
419
+ * {@link MAX_REPLAYED_UNITS}, and non-zero even for a pass that established
420
+ * nothing: it reports the work done, not the work kept.
421
+ */
422
+ readonly unitsReplayed: number
423
+ }
424
+
425
+ /**
426
+ * One path's state part-way through a walk of the transcript.
427
+ *
428
+ * `content` is a body this pass can rebuild and is still holding; `seen` is a
429
+ * path the conversation mutated whose current body it cannot rebuild. `seen`
430
+ * exists so that an edit cannot stack onto a body something unseen has already
431
+ * replaced; it reaches the ledger as nothing at all.
432
+ */
433
+ type Claim =
434
+ | { readonly kind: 'content'; body: string; readonly steps: LedgerStep[] }
435
+ | { readonly kind: 'seen' }
436
+
437
+ /** A call to make against the ledger once the whole walk has stood up. */
438
+ type LedgerStep =
439
+ | { readonly kind: 'write'; readonly body: string; readonly callId: string }
440
+ | { readonly kind: 'edit'; readonly content: string; readonly callId: string }
441
+
442
+ /**
443
+ * Rebuild an observation ledger from a conversation's own history.
444
+ *
445
+ * The ledger is process state. A resumed conversation gets a fresh empty one,
446
+ * and until something reads a file again the runtime knows nothing about files
447
+ * this conversation wrote in full — so the projection admits nothing and the
448
+ * model re-reads a body already in front of it, every time a session is picked
449
+ * back up.
450
+ *
451
+ * What is reconstructable here is exactly what the projection would admit, by
452
+ * the same predicates and the same bounded replay, because both go through the
453
+ * functions above. A `write` whose call and successful receipt are both intact
454
+ * hands back its body and its witness; the edits on top of it are replayed
455
+ * hop by hop and extend the chain. Anything else about a path the conversation
456
+ * mutated — a receipt compaction cleared, a call the transcript never answered,
457
+ * a mutation it refused, one a `pre_tool_use` hook skipped before it ever ran,
458
+ * a hop that no longer applies, a body past the bounds, a call too long to
459
+ * quote back — withdraws whatever this pass was holding for that path and
460
+ * writes NOTHING for it.
461
+ *
462
+ * Content-backed observations only, and that is the whole of what a resume
463
+ * restores. A path in the ledger with no fingerprint is a path `write` lets
464
+ * through unread: the read-before-overwrite refusal is `hasRead`, and the drift
465
+ * comparison it guards needs a body to compare. Seeding membership alone would
466
+ * therefore admit a full overwrite of a file that changed while the session was
467
+ * closed, with nothing checked — weaker than the empty ledger a resume used to
468
+ * start from, which refuses that overwrite outright. So a path this pass cannot
469
+ * rebuild is left exactly as that empty ledger leaves it, and the first real
470
+ * read re-establishes it.
471
+ *
472
+ * Two things this deliberately does not do. It never reads the filesystem: a
473
+ * fingerprint restored here is a claim derived from history, and the built-in
474
+ * mutation checks still compare it against the real file before anything is
475
+ * written — so a file changed while the session was closed is refused exactly
476
+ * as it is today, and the refusal's drift flag withdraws the entry. And it
477
+ * never recovers a body from a `read` receipt by undoing the line numbering: a
478
+ * read's rendering is compared, whole, against a body this pass already holds,
479
+ * and a read that cannot be matched that way withdraws the body rather than
480
+ * supplying one.
481
+ *
482
+ * That first guarantee is the reason `keyOf` is a parameter. Everything written
483
+ * here has to land on the key the mutation tools will look it up under, and on
484
+ * a host those keys are canonical — `write` and `edit` follow every symlink
485
+ * before they touch the ledger. Keying a seed lexically instead writes entries
486
+ * into a key space the tools never read: the projection would then match a
487
+ * fingerprint this pass had just written to it, and the drift refusal that is
488
+ * supposed to withdraw the claim would land somewhere else and never reach it.
489
+ * So the caller resolves the paths the way the tools do — see
490
+ * `file-evidence-seed.ts` — and a path it could not resolve gets no key, which
491
+ * makes the mutation that named it unattributable and stops the pass.
492
+ */
493
+ export function replayObservationLedger(
494
+ messages: readonly Message[],
495
+ tracker: FileReadTracker,
496
+ keyOf: FileKeyResolver,
497
+ attributions: PathAttributions = createPathAttributions(),
498
+ ): LedgerReplayReport {
499
+ const history = indexVisibleHistory(messages, keyOf)
500
+ const budget = createReplayBudget()
501
+ const claims = new Map<string, Claim>()
502
+ // The keys currently holding a body, newest last — the projection's own
503
+ // order, so the paths this keeps are the paths it would emit.
504
+ const withBody = new Set<string>()
505
+
506
+ for (const message of messages) {
507
+ if (message.role !== 'assistant') continue
508
+ for (const call of message.toolCalls ?? []) {
509
+ const name = call.function.name
510
+ if (name !== 'write' && name !== 'edit' && name !== 'read') continue
511
+ const receipt = history.results.get(call.id)
512
+ // Two receipts wearing one call id hide which of them this call got.
513
+ // On a mutation that is the ambiguity below, arriving from the other
514
+ // side. On a READ it is no smaller: the receipt that was hidden could
515
+ // be the one showing a body this walk is holding to be something else,
516
+ // and skipping the read past would keep a claim that very observation
517
+ // withdrew. Both get the same answer.
518
+ if (receipt === null) return commit(tracker, new Map(), budget)
519
+ const answered = receipt !== undefined && receipt.role === 'tool' && !receipt.isError
520
+ if (name === 'read') {
521
+ // A read that never came back, or came back refused, saw no body: it
522
+ // can neither confirm what this pass holds nor contradict it. And a
523
+ // read changes no file, so a claim left standing across one is still a
524
+ // claim about the same bytes.
525
+ if (answered) observeRead(history, call, claims, withBody, budget)
526
+ continue
527
+ }
528
+ // One id claimed by two calls hides which of them ran. On a path this
529
+ // pass is not holding that is merely invisible, but a mutation could
530
+ // have replaced a body claimed somewhere else in this walk, and there
531
+ // is no way to tell where — so the pass establishes nothing at all
532
+ // rather than carry a body something unseen may have moved past.
533
+ if (history.calls.get(call.id) === null) return commit(tracker, new Map(), budget)
534
+ // Attribution before outcome, for every mutation the walk reaches. A call
535
+ // the transcript never answered — the unknown-outcome result the kernel's
536
+ // own repair writes for one included — is precisely the call whose effect
537
+ // on the file nobody knows: the process may have died with the write half
538
+ // made. And a refusal is a tool's own report about this path, a drift
539
+ // refusal above all, which read the disk and found the body this ledger
540
+ // holds is not the body there. Carrying a claim through either would have
541
+ // the first resumed request tell the model a file holds a body the
542
+ // transcript itself says nobody can vouch for, so each withdraws the path
543
+ // it names — and only that path. A mutation attributable to no path at all
544
+ // is still the one thing that stops the pass, because then there is no
545
+ // path to withdraw.
546
+ const key = mutatedKey(history, call, attributions)
547
+ if (key === undefined) return commit(tracker, new Map(), budget)
548
+ const claim: Claim = answered
549
+ ? mutate(history, call, name, key, claims.get(key), budget)
550
+ : { kind: 'seen' }
551
+ claims.set(key, claim)
552
+ withBody.delete(key)
553
+ if (claim.kind === 'content') withBody.add(key)
554
+ // Oldest body first, matching what the projection keeps. An evicted
555
+ // path is one this pass stops vouching for: it was mutated here, and
556
+ // what it holds is past what the pass can carry, so it reaches the
557
+ // ledger as the nothing every unrebuilt path reaches it as.
558
+ if (withBody.size > MAX_WITNESSED_PATHS) {
559
+ const oldest = withBody.values().next().value as string
560
+ withBody.delete(oldest)
561
+ // Setting an existing key leaves it where it is, so the order the
562
+ // ledger is written in stays the order the calls arrived in.
563
+ claims.set(oldest, { kind: 'seen' })
564
+ }
565
+ }
566
+ }
567
+
568
+ return commit(tracker, claims, budget)
569
+ }
570
+
571
+ /** Apply a walk's surviving claims to the ledger, in the order they were made. */
572
+ function commit(
573
+ tracker: FileReadTracker,
574
+ claims: ReadonlyMap<string, Claim>,
575
+ budget: ReplayBudget,
576
+ ): LedgerReplayReport {
577
+ let pathsWitnessed = 0
578
+ let pathsSeen = 0
579
+ for (const [key, claim] of claims) {
580
+ if (claim.kind === 'seen') {
581
+ // Nothing. Not even membership: `hasRead` is the read-before-overwrite
582
+ // refusal, and granting it without a fingerprint would let a full
583
+ // overwrite of this path through with no body to compare — over a file
584
+ // that may have moved while the session was closed. The claim was
585
+ // carried through the walk so that no edit stacked onto a body
586
+ // something unseen had replaced; having done that, it is dropped.
587
+ pathsSeen++
588
+ continue
589
+ }
590
+ for (const step of claim.steps) {
591
+ if (step.kind === 'write') {
592
+ tracker.recordRead(key, step.body, step.callId)
593
+ } else if (tracker.recordEdit) {
594
+ tracker.recordEdit(key, step.content, step.callId)
595
+ } else {
596
+ // The same fallback the `edit` tool takes against a tracker without
597
+ // the method: the observation advances and no chain is built.
598
+ tracker.recordRead(key, step.content)
599
+ }
600
+ }
601
+ pathsWitnessed++
602
+ }
603
+ return { pathsWitnessed, pathsSeen, unitsReplayed: MAX_REPLAYED_UNITS - budget.remaining }
604
+ }
605
+
606
+ /** Fold one successful mutation into the path's claim. */
607
+ function mutate(
608
+ history: VisibleHistory,
609
+ call: ToolCall,
610
+ name: 'write' | 'edit',
611
+ key: string,
612
+ claim: Claim | undefined,
613
+ budget: ReplayBudget,
614
+ ): Claim {
615
+ if (name === 'write') {
616
+ const written = visibleWrite(history, call.id)
617
+ // A write nothing may quote back is still a write that HAPPENED, and the
618
+ // path it happened to is known: arguments past the evidence bound, a call
619
+ // the stream truncated, a path longer than an entry goes out with, a key
620
+ // the call and the ledger disagree about. Each of those withdraws the
621
+ // body and keeps the walk going — what was on this path is now unknown,
622
+ // which is a statement about one path rather than a reason to stop. A
623
+ // call a hook SKIPPED lands here too, from the other direction: that one
624
+ // never ran, so the file holds whatever it held before and this pass
625
+ // cannot say what that was either.
626
+ if (!written || written.key !== key) return { kind: 'seen' }
627
+ if (!spend(budget, written.body.length)) return { kind: 'seen' }
628
+ // A full body replaces everything under it, chain included.
629
+ return {
630
+ kind: 'content',
631
+ body: written.body,
632
+ steps: [{ kind: 'write', body: written.body, callId: call.id }],
633
+ }
634
+ }
635
+ // An edit onto a body this pass is not holding cannot be replayed onto
636
+ // anything, and one onto a chain already at its bound stops being followed.
637
+ if (!claim || claim.kind !== 'content') return { kind: 'seen' }
638
+ if (claim.steps.length > MAX_EDIT_CALLS) return { kind: 'seen' }
639
+ const hop = visibleEdit(history, call.id)
640
+ if (!hop || hop.key !== key) return { kind: 'seen' }
641
+ const replayed = replayHop(claim.body, hop.input, budget)
642
+ if (replayed === undefined) return { kind: 'seen' }
643
+ claim.body = replayed
644
+ claim.steps.push({ kind: 'edit', content: replayed, callId: call.id })
645
+ return claim
646
+ }
647
+
648
+ /**
649
+ * Confirm or withdraw a body this pass holds, from a `read` that observed it.
650
+ *
651
+ * A read changes no file, so on its own it establishes nothing: a partial read
652
+ * shows a window, and a body cannot be recovered from the rendering without
653
+ * stripping the line numbers back off — which would be inventing a file from
654
+ * text that was formatted for a reader, and is exactly what the ledger's
655
+ * contract forbids. What a read CAN do is settle whether a body already
656
+ * reconstructed here is the one the read saw. The rendering is produced
657
+ * forwards, from the body in hand through the read tool's own renderer, and
658
+ * compared whole.
659
+ *
660
+ * Equality is conclusive because of the numbering. Every line of a rendering
661
+ * goes out behind its own `${n}\t`, so one rendering belongs to exactly one
662
+ * body and window; and the partial-view notice carries no such prefix on its
663
+ * lines, so a rendering that covers a whole file cannot be equal to one that
664
+ * was cut short. A read this cannot match leaves the path known and its body
665
+ * withdrawn — the honest answer, and the one the drift check then re-derives
666
+ * from disk.
667
+ */
668
+ function observeRead(
669
+ history: VisibleHistory,
670
+ call: ToolCall,
671
+ claims: Map<string, Claim>,
672
+ withBody: Set<string>,
673
+ budget: ReplayBudget,
674
+ ): void {
675
+ const read = readWindow(history, call)
676
+ if (!read) return
677
+ const claim = claims.get(read.key)
678
+ if (!claim || claim.kind !== 'content') return
679
+ const shown = history.results.get(call.id)?.content
680
+ if (visibleCall(history, call.id, 'read') && typeof shown === 'string') {
681
+ // A whole-file rendering is the body plus a number on every line, so one
682
+ // shorter than the body it would render cannot be equal to it. The cheap
683
+ // half of the comparison, taken before anything is measured.
684
+ const units =
685
+ shown.length < claim.body.length ? undefined : wholeFileRenderUnits(claim.body, read.window)
686
+ // The rendering is content this pass materialises, so it is charged like
687
+ // every other body — and charged for what it will actually come to,
688
+ // worked out from the body and the window while the string does not exist
689
+ // yet. Charging the RECEIPT's length was charging one number and building
690
+ // another: a receipt that had been rewritten short could have the pass
691
+ // build a rendering the ceiling had never been asked about.
692
+ if (units !== undefined && spend(budget, units)) {
693
+ const rendered = renderNumberedRead(claim.body, read.window)
694
+ if (!rendered.partial && rendered.output === shown) return
695
+ }
696
+ }
697
+ claims.set(read.key, { kind: 'seen' })
698
+ // Off the held list as well as out of the claim. A withdrawn path is one
699
+ // this pass is no longer carrying a body for, so counting it against the
700
+ // eviction bound would have a later mutation evict a path that IS still
701
+ // holding one — the pass would then write fewer witnesses than the
702
+ // projection can emit, and the one it dropped was admissible.
703
+ withBody.delete(read.key)
704
+ }
705
+
706
+ /**
707
+ * What `read` would return for this whole body, in UTF-16 units, without
708
+ * building it — and `undefined` for a window that would leave a line out.
709
+ *
710
+ * The rendering puts `${n}\t` in front of every line and joins them back with
711
+ * the newlines the body already carries, so its length is the body's, plus one
712
+ * tab per line, plus the digits of the numbers `1..lines`. A windowed read
713
+ * renumbers from its offset and carries the PARTIAL notice, and can never equal
714
+ * a whole body's rendering — so it is refused here rather than built and
715
+ * compared, which is also what makes the figure above exact.
716
+ */
717
+ function wholeFileRenderUnits(body: string, window: ReadWindowRequest): number | undefined {
718
+ const lines = countLines(body)
719
+ const { start, end } = resolveReadWindow(window, lines)
720
+ if (start !== 0 || end < lines) return undefined
721
+ return body.length + lines + lineNumberUnits(lines)
722
+ }
723
+
724
+ /** Lines the way `String.split('\n')` counts them, without building the array. */
725
+ function countLines(body: string): number {
726
+ let lines = 1
727
+ for (let at = body.indexOf('\n'); at !== -1; at = body.indexOf('\n', at + 1)) lines++
728
+ return lines
729
+ }
730
+
731
+ /** Units the line numbers `1..lines` occupy, counted by decade rather than one by one. */
732
+ function lineNumberUnits(lines: number): number {
733
+ let units = 0
734
+ for (let width = 1, first = 1; first <= lines; width++, first *= 10)
735
+ units += (Math.min(lines, first * 10 - 1) - first + 1) * width
736
+ return units
737
+ }
738
+
739
+ /**
740
+ * The ledger key a mutation belongs to, whatever the transcript says came back
741
+ * — or `undefined` for one this pass cannot attribute to any file at all.
742
+ *
743
+ * Deliberately the declared path rather than the tool's own schema, and asked
744
+ * before the outcome is. A call that fails validation in some OTHER field, that
745
+ * is too long to read as evidence, that was never answered or that was refused
746
+ * still says which file it was aimed at, and a mutation that can be attributed
747
+ * can be honoured — as a content-changing observation nothing here can replay,
748
+ * which costs the path it names and no other. A key is exactly what withdrawing
749
+ * one path rather than the whole pass requires. `undefined` is kept for the two
750
+ * things that really are unattributable: a call naming no path, and a path this
751
+ * run may not reach, whose key the resolver therefore withheld. Those, and only
752
+ * those, stop the pass — a refused call included, because a refusal this pass
753
+ * cannot place is a refusal it cannot act on either.
754
+ */
755
+ function mutatedKey(
756
+ history: VisibleHistory,
757
+ call: ToolCall,
758
+ attributions: PathAttributions,
759
+ ): string | undefined {
760
+ const path = declaredPath(call, attributions)
761
+ return path === undefined ? undefined : history.keyOf(path)
762
+ }
763
+
764
+ /** The path and window one `read` call asked for. */
765
+ function readWindow(
766
+ history: VisibleHistory,
767
+ call: ToolCall,
768
+ ): { readonly key: string; readonly window: ReadWindowRequest } | undefined {
769
+ const input = ReadFileTool.inputSchema.safeParse(parseArguments(call))
770
+ if (!input.success) return undefined
771
+ const key = history.keyOf(input.data.path)
772
+ // A read this pass cannot key cannot contradict a claim either: the only
773
+ // claims it holds came from mutations, and a mutation whose path would
774
+ // not resolve stopped the pass before it made one.
775
+ return key === undefined ? undefined : { key, window: input.data }
776
+ }