@namzu/sdk 20.4.0 → 21.1.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 (98) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/dist/bridge/sse/mapper.d.ts +19 -0
  3. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  4. package/dist/bridge/sse/mapper.js +9 -1
  5. package/dist/bridge/sse/mapper.js.map +1 -1
  6. package/dist/contracts/schemas.d.ts +10 -10
  7. package/dist/manager/run/persistence.d.ts +26 -0
  8. package/dist/manager/run/persistence.d.ts.map +1 -1
  9. package/dist/manager/run/persistence.js +40 -0
  10. package/dist/manager/run/persistence.js.map +1 -1
  11. package/dist/public-runtime.d.ts +8 -0
  12. package/dist/public-runtime.d.ts.map +1 -1
  13. package/dist/public-runtime.js +31 -0
  14. package/dist/public-runtime.js.map +1 -1
  15. package/dist/run/command-gate.d.ts +107 -0
  16. package/dist/run/command-gate.d.ts.map +1 -0
  17. package/dist/run/command-gate.js +157 -0
  18. package/dist/run/command-gate.js.map +1 -0
  19. package/dist/run/drain.d.ts +194 -0
  20. package/dist/run/drain.d.ts.map +1 -0
  21. package/dist/run/drain.js +228 -0
  22. package/dist/run/drain.js.map +1 -0
  23. package/dist/run/index.d.ts +8 -0
  24. package/dist/run/index.d.ts.map +1 -1
  25. package/dist/run/index.js +4 -0
  26. package/dist/run/index.js.map +1 -1
  27. package/dist/run/memory-promoter.d.ts +70 -0
  28. package/dist/run/memory-promoter.d.ts.map +1 -0
  29. package/dist/run/memory-promoter.js +117 -0
  30. package/dist/run/memory-promoter.js.map +1 -0
  31. package/dist/run/workspace-fingerprint.d.ts +105 -0
  32. package/dist/run/workspace-fingerprint.d.ts.map +1 -0
  33. package/dist/run/workspace-fingerprint.js +147 -0
  34. package/dist/run/workspace-fingerprint.js.map +1 -0
  35. package/dist/runtime/query/events.d.ts +12 -0
  36. package/dist/runtime/query/events.d.ts.map +1 -1
  37. package/dist/runtime/query/events.js +61 -3
  38. package/dist/runtime/query/events.js.map +1 -1
  39. package/dist/runtime/query/index.d.ts +36 -0
  40. package/dist/runtime/query/index.d.ts.map +1 -1
  41. package/dist/runtime/query/index.js +32 -0
  42. package/dist/runtime/query/index.js.map +1 -1
  43. package/dist/runtime/query/resume-run.d.ts +25 -0
  44. package/dist/runtime/query/resume-run.d.ts.map +1 -1
  45. package/dist/runtime/query/resume-run.js +15 -3
  46. package/dist/runtime/query/resume-run.js.map +1 -1
  47. package/dist/store/index.d.ts +1 -1
  48. package/dist/store/index.d.ts.map +1 -1
  49. package/dist/store/index.js +1 -1
  50. package/dist/store/index.js.map +1 -1
  51. package/dist/store/run/conformance.d.ts +159 -0
  52. package/dist/store/run/conformance.d.ts.map +1 -0
  53. package/dist/store/run/conformance.js +451 -0
  54. package/dist/store/run/conformance.js.map +1 -0
  55. package/dist/store/run/disk.d.ts +30 -2
  56. package/dist/store/run/disk.d.ts.map +1 -1
  57. package/dist/store/run/disk.js +101 -0
  58. package/dist/store/run/disk.js.map +1 -1
  59. package/dist/store/run/memory.d.ts +5 -4
  60. package/dist/store/run/memory.d.ts.map +1 -1
  61. package/dist/store/run/memory.js +30 -1
  62. package/dist/store/run/memory.js.map +1 -1
  63. package/dist/types/doctor/check.d.ts +28 -2
  64. package/dist/types/doctor/check.d.ts.map +1 -1
  65. package/dist/types/run/event-cursor.d.ts +90 -0
  66. package/dist/types/run/event-cursor.d.ts.map +1 -0
  67. package/dist/types/run/event-cursor.js +47 -0
  68. package/dist/types/run/event-cursor.js.map +1 -0
  69. package/dist/types/run/events.d.ts +71 -1
  70. package/dist/types/run/events.d.ts.map +1 -1
  71. package/dist/types/run/events.js.map +1 -1
  72. package/dist/types/run/index.d.ts +1 -0
  73. package/dist/types/run/index.d.ts.map +1 -1
  74. package/dist/types/run/index.js +1 -0
  75. package/dist/types/run/index.js.map +1 -1
  76. package/dist/types/run/store.d.ts +41 -1
  77. package/dist/types/run/store.d.ts.map +1 -1
  78. package/package.json +6 -1
  79. package/src/bridge/sse/mapper.ts +28 -1
  80. package/src/manager/run/persistence.ts +43 -0
  81. package/src/public-runtime.ts +53 -0
  82. package/src/run/command-gate.ts +234 -0
  83. package/src/run/drain.ts +393 -0
  84. package/src/run/index.ts +20 -0
  85. package/src/run/memory-promoter.ts +155 -0
  86. package/src/run/workspace-fingerprint.ts +193 -0
  87. package/src/runtime/query/events.ts +68 -4
  88. package/src/runtime/query/index.ts +87 -0
  89. package/src/runtime/query/resume-run.ts +67 -12
  90. package/src/store/index.ts +1 -1
  91. package/src/store/run/conformance.ts +705 -0
  92. package/src/store/run/disk.ts +106 -2
  93. package/src/store/run/memory.ts +35 -5
  94. package/src/types/doctor/check.ts +28 -2
  95. package/src/types/run/event-cursor.ts +118 -0
  96. package/src/types/run/events.ts +80 -3
  97. package/src/types/run/index.ts +1 -0
  98. package/src/types/run/store.ts +43 -1
@@ -5,6 +5,25 @@ import type { RunEvent } from '../../types/run/events.js'
5
5
  export interface MappedStreamEvent {
6
6
  wire: StreamEventType
7
7
  data: Record<string, unknown>
8
+ /**
9
+ * The cursor a client resubscribes at, as `<runId>:<seq>`.
10
+ *
11
+ * Not a bare number, and the reason is structural: a parent's stream also
12
+ * carries its children's events, each numbered in its OWN run's log, so one
13
+ * scalar over a mixed stream would compare positions from two different
14
+ * sequences. The run id is what makes the position addressable — a client
15
+ * keeps one cursor per run id and sends the right one back.
16
+ *
17
+ * This is what an SSE `id:` line should carry, which is why it sits beside
18
+ * the payload rather than inside it: a framer writes it without having to
19
+ * understand what kind of event it is.
20
+ *
21
+ * Absent when the event is not recoverable — every ephemeral event, every
22
+ * event whose durable write failed, and every delegation-lifecycle event
23
+ * that never passed through the run's log at all. A client must not advance
24
+ * its cursor on one, and the absence is how it knows.
25
+ */
26
+ id?: string
8
27
  }
9
28
 
10
29
  type EventTransform<K extends RunEvent['type']> = {
@@ -525,7 +544,15 @@ export function mapRunToStreamEvent(event: RunEvent, runId: RunId): MappedStream
525
544
  data.parent_task_id = annotated.parentTaskId
526
545
  }
527
546
 
528
- return { wire: mapping.wire, data }
547
+ // Keyed on the event's OWN run id, not the stream's. A child's event
548
+ // arriving on a parent's stream is numbered in the child's log, so stamping
549
+ // the enclosing run here would produce a cursor that addresses the wrong
550
+ // sequence — and it would look right.
551
+ return {
552
+ wire: mapping.wire,
553
+ data,
554
+ ...(event.seq !== undefined ? { id: `${event.runId}:${event.seq}` } : {}),
555
+ }
529
556
  }
530
557
 
531
558
  /** @deprecated Use mapRunToStreamEvent */
@@ -174,11 +174,54 @@ export class RunPersistence {
174
174
  return this.runStore.getRunDir()
175
175
  }
176
176
 
177
+ /**
178
+ * Highest sequence already in this run's durable event log.
179
+ *
180
+ * Seeded by {@link init} and advanced by {@link commitEventSeq}. Held here
181
+ * rather than on the event translator because the translator is built
182
+ * before `init` has run and would need a second, forgettable hop to learn
183
+ * the seed — where `init` is already mandatory, already awaited, and
184
+ * already the thing that binds the store.
185
+ */
186
+ private _lastEventSeq = 0
187
+
188
+ /** Highest sequence this run's log holds. Zero before anything is recorded. */
189
+ get lastEventSeq(): number {
190
+ return this._lastEventSeq
191
+ }
192
+
177
193
  async init(): Promise<void> {
178
194
  await this.runStore.initRun(this.run.id, this.run.parentRunId)
195
+ // A resume reuses the run id and therefore the same log, so the counter
196
+ // has to continue that log rather than start a second sequence inside
197
+ // it. Without this, a run recalled after a crash re-numbers from 1 and a
198
+ // consumer holding a cursor at 40 is told, truthfully and uselessly,
199
+ // that there is nothing above it.
200
+ const existing = await this.runStore.readEvents()
201
+ this._lastEventSeq = existing.at(-1)?.seq ?? 0
179
202
  await this.runStore.writeRunMeta(this.run)
180
203
  }
181
204
 
205
+ /**
206
+ * The number the NEXT durable event would take. Not yet taken — see
207
+ * {@link commitEventSeq}.
208
+ */
209
+ nextEventSeq(): number {
210
+ return this._lastEventSeq + 1
211
+ }
212
+
213
+ /**
214
+ * Take the candidate number, once its event is actually in the log.
215
+ *
216
+ * Split from {@link nextEventSeq} so a failed append consumes nothing: a
217
+ * `seq` is a claim that the event is recoverable, and advancing the counter
218
+ * before the write lands would leave a hole no event fills and hand a live
219
+ * consumer a cursor pointing at it.
220
+ */
221
+ commitEventSeq(seq: number): void {
222
+ if (seq > this._lastEventSeq) this._lastEventSeq = seq
223
+ }
224
+
182
225
  markRunning(): void {
183
226
  this.run.status = 'running'
184
227
  }
@@ -128,6 +128,50 @@ export {
128
128
  checkLimitsDetailed,
129
129
  createRunReporter,
130
130
  } from './run/index.js'
131
+ // One bounded pass over a queue of durable runs: list what nobody holds,
132
+ // claim it, work it, release it in a `finally`. Every primitive it composes
133
+ // already shipped and nothing composed them, so an approval inbox and a
134
+ // crash sweeper each still needed a host to write the loop — including the
135
+ // two parts a host writes wrong, the release on the failure path and the
136
+ // `null` claim that is not one. Not a daemon: it makes one pass and returns.
137
+ export { DEFAULT_DRAIN_PAGE_SIZE, drainRuns } from './run/index.js'
138
+ export type {
139
+ DrainFailure,
140
+ DrainRun,
141
+ DrainRunsParams,
142
+ DrainRunsResult,
143
+ } from './run/index.js'
144
+ // A `ReviewAnswer` that runs shell commands, so "don't finish until the
145
+ // build passes" needs no TypeScript. `reviewAnswer` was the seam for this
146
+ // and nothing shipped supplied one. Skips re-running a command whose
147
+ // failure the workspace has not changed since — the difference between a
148
+ // bounded loop and one that spends its whole budget confirming a failure it
149
+ // already reported.
150
+ export {
151
+ DEFAULT_GATE_MAX_RETRIES,
152
+ DEFAULT_GATE_OUTPUT_CHARS,
153
+ DEFAULT_GATE_TIMEOUT_MS,
154
+ FINGERPRINT_MAX_BYTES,
155
+ FINGERPRINT_TIMEOUT_MS,
156
+ clipOutput,
157
+ createCommandGate,
158
+ fingerprintWorkspace,
159
+ } from './run/index.js'
160
+ export type {
161
+ CommandGateOptions,
162
+ FingerprintExec,
163
+ GateExec,
164
+ WorkspaceFingerprintOptions,
165
+ } from './run/index.js'
166
+ // The default `promoteMemory`: write what a run learned into a MemoryStore,
167
+ // or write NOTHING. The hook was invoked at settle with the compaction
168
+ // extractor's already-structured output and no shipped app supplied it, so
169
+ // that structure was serialized into one system message and dropped when
170
+ // the run ended. A run that learned nothing leaves no record at all — the
171
+ // model reads this store, so noise here is context spent on a run that did
172
+ // nothing.
173
+ export { RUN_MEMORY_TAG, createMemoryPromoter } from './run/index.js'
174
+ export type { MemoryPromoterOptions } from './run/index.js'
131
175
 
132
176
  // ─── personas, skills, advisory ──────────────────────────────────────────
133
177
 
@@ -223,6 +267,15 @@ export {
223
267
  // two workers restore one checkpoint, both run its tools and both write under
224
268
  // one run id — which loses half the work and reports nothing.
225
269
  export { claimRun, fencedOut, releaseRun, toClaimSummary } from './store/index.js'
270
+ // Reading a run's durable event log back — what a consumer that lost its
271
+ // connection catches up through. `readRunEventsIn` takes a directory rather
272
+ // than a bound store because binding one CREATES the run directory, and a read
273
+ // that mints an empty run then reports it as having no events is worse than an
274
+ // error. `resolveRunEventReplay` decides what a cursor is owed, and REFUSES
275
+ // rather than delivering a partial catch-up a consumer would fold into its
276
+ // state without knowing it had a hole in it.
277
+ export { readRunEventsIn } from './store/index.js'
278
+ export { resolveRunEventReplay } from './types/run/event-cursor.js'
226
279
 
227
280
  export {
228
281
  AgentRegistry,
@@ -0,0 +1,234 @@
1
+ /**
2
+ * A {@link ReviewAnswer} that runs shell commands and hands the failure back.
3
+ *
4
+ * `reviewAnswer` was the seam for exactly this — judge the answer at the point
5
+ * the model stops calling tools, and return it with feedback instead of
6
+ * settling — and nothing shipped supplied one, so an operator who wanted
7
+ * "don't finish until the build passes" had to write TypeScript. This is the
8
+ * supplier. With it, `--gate 'pnpm test'` is the whole unattended story: the
9
+ * model works, stops, the tests run, and a failure comes back as the next user
10
+ * turn rather than as a green run somebody discovers in CI.
11
+ *
12
+ * The kernel already bounds it. The reviewer is consulted only when the model
13
+ * stopped calling tools, never on the forced-final turn, and a rejection
14
+ * budget stops the run with `answer_rejected` — a stop reason that names the
15
+ * reviewer rather than blaming a token budget. None of that is re-implemented
16
+ * here.
17
+ *
18
+ * ## The part that is not just "run a command"
19
+ *
20
+ * **Before re-running a command that already failed, the workspace is
21
+ * fingerprinted, and an identical fingerprint means the command is NOT run.**
22
+ *
23
+ * This is the difference between a bounded loop and one that spends its whole
24
+ * budget. A model that has run out of ideas answers again without editing
25
+ * anything; re-running the suite then costs a full execution — often the most
26
+ * expensive thing in the loop — to produce a failure already known character
27
+ * for character. Worse, the feedback is identical, so the model is handed the
28
+ * same prompt that just failed to help it. Saying instead "the workspace has
29
+ * not changed since that failure; edit something before trying to finish"
30
+ * is both cheaper and a different instruction.
31
+ *
32
+ * The attempt still advances. Skipping the command is a saving, not a pardon:
33
+ * an answer that changed nothing has been rejected, and the run's budget must
34
+ * see that or a stuck model loops forever for free.
35
+ *
36
+ * And it fails open on the cheap side. No fingerprint — a git invocation that
37
+ * errored, a timeout, output past the cap, a tree with no commits — means the
38
+ * command runs. See {@link fingerprintWorkspace}: the cost of re-running
39
+ * unnecessarily is one execution; the cost of wrongly skipping is a
40
+ * verification that silently did not happen.
41
+ */
42
+
43
+ import { LocalExecutionContext } from '../execution/local.js'
44
+ import type { CommandOptions, CommandResult } from '../types/execution/index.js'
45
+ import type { AnswerReview, ReviewAnswer } from '../types/run/answer-review.js'
46
+ import { fingerprintWorkspace } from './workspace-fingerprint.js'
47
+
48
+ /** How the gate runs a command. Injected so a test needs no shell. */
49
+ export type GateExec = (
50
+ command: string,
51
+ args: string[],
52
+ options?: CommandOptions,
53
+ ) => Promise<CommandResult>
54
+
55
+ /** Default per-command deadline. A test suite is allowed to be slow. */
56
+ export const DEFAULT_GATE_TIMEOUT_MS = 600_000
57
+
58
+ /** How many attempts the gate will EXECUTE its commands for, by default. */
59
+ export const DEFAULT_GATE_MAX_RETRIES = 3
60
+
61
+ /**
62
+ * Model-visible characters of a failing command's output.
63
+ *
64
+ * Head and tail, not head alone: a compiler names the file at the top and a
65
+ * test runner names the failure at the bottom, and a gate that only ever kept
66
+ * one end would be useless for one of them.
67
+ */
68
+ export const DEFAULT_GATE_OUTPUT_CHARS = 4_000
69
+
70
+ export interface CommandGateOptions {
71
+ /**
72
+ * Shell command lines, run in order, stopping at the first failure.
73
+ *
74
+ * In order and short-circuiting because that is what a person means by
75
+ * "typecheck then test": a type error makes the test output noise about
76
+ * the same cause, and handing the model both invites it to fix the
77
+ * symptom.
78
+ */
79
+ readonly commands: readonly string[]
80
+ /** Directory the commands run in, and the tree that is fingerprinted. */
81
+ readonly cwd: string
82
+ /**
83
+ * How many attempts will actually EXECUTE the commands.
84
+ *
85
+ * Past it the gate rejects without running anything, naming the
86
+ * exhaustion. It does not accept: an answer that never passed the gate
87
+ * has not passed the gate, and a reviewer that gave up by accepting would
88
+ * hand back a green run over a red build — the exact outcome the gate
89
+ * exists to prevent. What ENDS the run is the kernel's rejection budget,
90
+ * so set that to the same number (the CLI does).
91
+ */
92
+ readonly maxRetries?: number
93
+ /** Per-command deadline. See {@link DEFAULT_GATE_TIMEOUT_MS}. */
94
+ readonly timeoutMs?: number
95
+ /** Override the executor. Defaults to a local shell in `cwd`. */
96
+ readonly exec?: GateExec
97
+ /** See {@link DEFAULT_GATE_OUTPUT_CHARS}. */
98
+ readonly maxOutputChars?: number
99
+ /**
100
+ * Override the change detector. Defaults to
101
+ * {@link fingerprintWorkspace} over `cwd`.
102
+ *
103
+ * Returning `null` means "cannot tell", and the gate then runs its
104
+ * commands. A detector that returned a constant would silence the gate
105
+ * after its first failure, so this seam exists for tests and for a host
106
+ * whose workspace is not a git tree — not as a way to turn the check off.
107
+ */
108
+ readonly fingerprint?: () => Promise<string | null>
109
+ }
110
+
111
+ /** Head and tail of a command's output, with the middle marked as dropped. */
112
+ export function clipOutput(text: string, max: number): string {
113
+ const trimmed = text.trimEnd()
114
+ if (trimmed.length <= max) return trimmed
115
+ const half = Math.floor(max / 2)
116
+ const dropped = trimmed.length - half * 2
117
+ return `${trimmed.slice(0, half)}\n… ${dropped} characters omitted …\n${trimmed.slice(-half)}`
118
+ }
119
+
120
+ interface LastFailure {
121
+ readonly command: string
122
+ /** The tree as it stood when this failed. `null` = could not be taken. */
123
+ readonly fingerprint: string | null
124
+ }
125
+
126
+ function failureFeedback(
127
+ command: string,
128
+ attempt: number,
129
+ result: CommandResult,
130
+ maxOutputChars: number,
131
+ ): string {
132
+ const output = clipOutput(`${result.stdout}\n${result.stderr}`, maxOutputChars)
133
+ return [
134
+ `The answer was not accepted: \`${command}\` failed (attempt ${attempt}, exit ${result.exitCode}).`,
135
+ '',
136
+ 'Output:',
137
+ '```',
138
+ output || '(no output)',
139
+ '```',
140
+ '',
141
+ 'Fix the cause and then finish. Do not restate the failure back to me; change the code so the command passes.',
142
+ ].join('\n')
143
+ }
144
+
145
+ function unchangedFeedback(command: string, attempt: number): string {
146
+ return [
147
+ `The answer was not accepted, and \`${command}\` was NOT re-run (attempt ${attempt}).`,
148
+ '',
149
+ 'The workspace is byte-for-byte identical to what it was when that command last failed — no file was created, edited or deleted since. Running it again would produce the failure you have already been shown.',
150
+ '',
151
+ 'Edit something before trying to finish again. If you believe the change you described was made, verify it by reading the file: it is not on disk.',
152
+ ].join('\n')
153
+ }
154
+
155
+ function exhaustedFeedback(command: string, maxRetries: number): string {
156
+ return [
157
+ `The answer was not accepted: \`${command}\` has failed and this gate has spent its ${maxRetries} attempts.`,
158
+ '',
159
+ 'No further command will be run.',
160
+ ].join('\n')
161
+ }
162
+
163
+ /**
164
+ * Build a reviewer that accepts an answer only when every command passes.
165
+ *
166
+ * Stateful across calls within one run, deliberately: the whole point is that
167
+ * attempt N+1 can be compared with attempt N. Build one gate per run.
168
+ */
169
+ export function createCommandGate(options: CommandGateOptions): ReviewAnswer {
170
+ const { commands, cwd } = options
171
+ const timeoutMs = options.timeoutMs ?? DEFAULT_GATE_TIMEOUT_MS
172
+ const maxRetries = options.maxRetries ?? DEFAULT_GATE_MAX_RETRIES
173
+ const maxOutputChars = options.maxOutputChars ?? DEFAULT_GATE_OUTPUT_CHARS
174
+
175
+ // Built once and reused: constructing a context per attempt would re-stat
176
+ // the directory for no gain, and the context holds nothing per-run.
177
+ const context = new LocalExecutionContext({ id: 'namzu-command-gate', cwd })
178
+ const exec: GateExec =
179
+ options.exec ?? ((command, args, opts) => context.executeCommand(command, args, opts))
180
+ const fingerprint =
181
+ options.fingerprint ?? (() => fingerprintWorkspace({ cwd, exec, timeoutMs: 20_000 }))
182
+
183
+ let attempt = 0
184
+ let last: LastFailure | undefined
185
+ let executions = 0
186
+
187
+ return async (): Promise<AnswerReview> => {
188
+ attempt += 1
189
+
190
+ // A gate that already failed, over a tree nothing has touched since.
191
+ // The command is skipped, and the attempt still counts.
192
+ //
193
+ // The comparison is a bare `===` rather than `now !== null && now ===
194
+ // …`. The guard above already establishes that the recorded
195
+ // fingerprint is non-null, so a `null` from this call cannot match it,
196
+ // and the extra clause was a branch nothing could reach — a mutation
197
+ // that deleted it killed no test, which is what a dead condition looks
198
+ // like from the outside.
199
+ if (last && last.fingerprint !== null) {
200
+ const now = await fingerprint()
201
+ if (now === last.fingerprint) {
202
+ return { accept: false, feedback: unchangedFeedback(last.command, attempt) }
203
+ }
204
+ }
205
+
206
+ if (last && executions >= maxRetries) {
207
+ return { accept: false, feedback: exhaustedFeedback(last.command, maxRetries) }
208
+ }
209
+
210
+ executions += 1
211
+ for (const command of commands) {
212
+ // `shell: true` because the operator handed over a command LINE —
213
+ // `pnpm test -- --run`, with its flags and its quoting — and taking
214
+ // that as an executable name plus literal arguments would fail on
215
+ // every gate anyone would actually write. Explicit, per the note on
216
+ // `LocalExecutionContext.executeCommand`: shell interpretation is
217
+ // opt-in, and this is the opt-in.
218
+ const result = await exec(command, [], { cwd, timeoutMs, shell: true })
219
+ if (result.exitCode === 0) continue
220
+
221
+ // Taken AFTER the failure, not before the run: the comparison next
222
+ // time is against the tree this verdict was formed over. A snapshot
223
+ // from before the command would miss anything the command itself
224
+ // wrote — a formatter, a snapshot updater, a lockfile.
225
+ last = { command, fingerprint: await fingerprint() }
226
+ return { accept: false, feedback: failureFeedback(command, attempt, result, maxOutputChars) }
227
+ }
228
+
229
+ // Cleared, so a later rejection by a DIFFERENT command is not compared
230
+ // against a tree this one failed over.
231
+ last = undefined
232
+ return { accept: true }
233
+ }
234
+ }