@namzu/sdk 62.6.1 → 62.8.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.
- package/CHANGELOG.md +42 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +5 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +1 -0
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/explore.d.ts.map +1 -1
- package/dist/agents/explore.js +2 -1
- package/dist/agents/explore.js.map +1 -1
- package/dist/compaction/index.d.ts +1 -1
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/model-summary.js +2 -2
- package/dist/compaction/model-summary.js.map +1 -1
- package/dist/compaction/summary.d.ts +16 -1
- package/dist/compaction/summary.d.ts.map +1 -1
- package/dist/compaction/summary.js +25 -2
- package/dist/compaction/summary.js.map +1 -1
- package/dist/config/runtime.d.ts +8 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +15 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +3 -0
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +8 -0
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/manager/agent/child-session.d.ts +12 -0
- package/dist/manager/agent/child-session.d.ts.map +1 -1
- package/dist/manager/agent/child-session.js +29 -0
- package/dist/manager/agent/child-session.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +8 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +13 -2
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +107 -29
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +5 -3
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +5 -1
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/retry.d.ts +25 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +28 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +4 -2
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +5 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/runtime/query/auto-review/classifier.d.ts +7 -1
- package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/classifier.js +8 -4
- package/dist/runtime/query/auto-review/classifier.js.map +1 -1
- package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/prompt.js +2 -0
- package/dist/runtime/query/auto-review/prompt.js.map +1 -1
- package/dist/runtime/query/auto-review/script-evidence.d.ts +93 -0
- package/dist/runtime/query/auto-review/script-evidence.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/script-evidence.js +566 -0
- package/dist/runtime/query/auto-review/script-evidence.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +13 -0
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +37 -0
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +28 -2
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +38 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +154 -13
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +7 -1
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +2 -0
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +8 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +7 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +32 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +158 -10
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +12 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +48 -1
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +5 -0
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +3 -3
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
- package/dist/runtime/query/prelude-lease.js +22 -0
- package/dist/runtime/query/prelude-lease.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +30 -12
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +29 -0
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +44 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/query/turn/completion-audit.d.ts +27 -1
- package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -1
- package/dist/runtime/query/turn/completion-audit.js +71 -6
- package/dist/runtime/query/turn/completion-audit.js.map +1 -1
- package/dist/runtime/query/turn/outcome-ledger.d.ts +113 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -0
- package/dist/runtime/query/turn/outcome-ledger.js +578 -0
- package/dist/runtime/query/turn/outcome-ledger.js.map +1 -0
- package/dist/sandbox/file-walk.d.ts +7 -1
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +30 -4
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/glob-segment.d.ts +8 -1
- package/dist/sandbox/glob-segment.d.ts.map +1 -1
- package/dist/sandbox/glob-segment.js +28 -9
- package/dist/sandbox/glob-segment.js.map +1 -1
- package/dist/store/checkpoint/contract.d.ts +12 -0
- package/dist/store/checkpoint/contract.d.ts.map +1 -1
- package/dist/store/checkpoint/contract.js.map +1 -1
- package/dist/store/checkpoint/disk.d.ts.map +1 -1
- package/dist/store/checkpoint/disk.js +34 -2
- package/dist/store/checkpoint/disk.js.map +1 -1
- package/dist/store/checkpoint/prune.d.ts.map +1 -1
- package/dist/store/checkpoint/prune.js +6 -0
- package/dist/store/checkpoint/prune.js.map +1 -1
- package/dist/store/evidence/disk.js +40 -3
- package/dist/store/evidence/disk.js.map +1 -1
- package/dist/store/session-log/incremental.d.ts +23 -0
- package/dist/store/session-log/incremental.d.ts.map +1 -0
- package/dist/store/session-log/incremental.js +117 -0
- package/dist/store/session-log/incremental.js.map +1 -0
- package/dist/store/session-log/index.d.ts +1 -0
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -0
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/store/session-log/turn-outcome.d.ts +37 -0
- package/dist/store/session-log/turn-outcome.d.ts.map +1 -0
- package/dist/store/session-log/turn-outcome.js +77 -0
- package/dist/store/session-log/turn-outcome.js.map +1 -0
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +1 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +11 -2
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +19 -1
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -1
- package/dist/tools/builtins/bounded-file-read.js +56 -3
- package/dist/tools/builtins/bounded-file-read.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +24 -3
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts +31 -17
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +92 -27
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/glob.js +1 -1
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +148 -21
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/notebook-guard.d.ts +19 -0
- package/dist/tools/builtins/notebook-guard.d.ts.map +1 -0
- package/dist/tools/builtins/notebook-guard.js +75 -0
- package/dist/tools/builtins/notebook-guard.js.map +1 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +8 -2
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +8 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -1
- package/dist/tools/builtins/search-support.js +66 -0
- package/dist/tools/builtins/search-support.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +14 -1
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +25 -6
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +6 -1
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/outcome-record.d.ts +22 -0
- package/dist/tools/coordinator/outcome-record.d.ts.map +1 -0
- package/dist/tools/coordinator/outcome-record.js +91 -0
- package/dist/tools/coordinator/outcome-record.js.map +1 -0
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +1 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts +23 -2
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +37 -2
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/tools/untrusted-envelope.d.ts +6 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +22 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +3 -4
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/toolsets/validation-issues.d.ts +27 -0
- package/dist/toolsets/validation-issues.d.ts.map +1 -0
- package/dist/toolsets/validation-issues.js +83 -0
- package/dist/toolsets/validation-issues.js.map +1 -0
- package/dist/turn/memory-recall.d.ts +13 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +18 -0
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/agent/base.d.ts +19 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/common/index.d.ts +17 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +8 -1
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/hitl/index.d.ts +22 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/config.d.ts +20 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +3 -3
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/index.d.ts +1 -0
- package/dist/types/session/index.d.ts.map +1 -1
- package/dist/types/session/index.js +1 -0
- package/dist/types/session/index.js.map +1 -1
- package/dist/types/session/outcome.d.ts +95 -0
- package/dist/types/session/outcome.d.ts.map +1 -0
- package/dist/types/session/outcome.js +45 -0
- package/dist/types/session/outcome.js.map +1 -0
- package/dist/types/session/prepare-step.d.ts +2 -0
- package/dist/types/session/prepare-step.d.ts.map +1 -1
- package/dist/types/session/records.d.ts +167 -2
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +42 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/turn.d.ts +18 -0
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +6 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +5 -0
- package/src/agents/examples/SupervisorAgent.ts +1 -0
- package/src/agents/explore.ts +2 -1
- package/src/compaction/index.ts +1 -0
- package/src/compaction/model-summary.ts +2 -2
- package/src/compaction/summary.ts +30 -2
- package/src/config/runtime.ts +15 -0
- package/src/execution/workflow/runtime.ts +4 -0
- package/src/execution/workflow/types.ts +9 -0
- package/src/manager/agent/child-session.ts +36 -0
- package/src/manager/agent/lifecycle.ts +9 -0
- package/src/manager/session/turn-recorder.ts +115 -30
- package/src/pricing/rates.source.json +1 -1
- package/src/prompt/coding-agent-doctrine.ts +5 -3
- package/src/provider/fallback.ts +6 -1
- package/src/provider/retry.ts +51 -0
- package/src/public-runtime.ts +10 -1
- package/src/public-tools.ts +6 -1
- package/src/runtime/query/auto-review/classifier.ts +21 -5
- package/src/runtime/query/auto-review/prompt.ts +2 -0
- package/src/runtime/query/auto-review/script-evidence.ts +618 -0
- package/src/runtime/query/checkpoint.ts +53 -1
- package/src/runtime/query/context.ts +33 -6
- package/src/runtime/query/executor.ts +183 -21
- package/src/runtime/query/finalize-turn.ts +8 -1
- package/src/runtime/query/index.ts +19 -0
- package/src/runtime/query/iteration/index.ts +167 -14
- package/src/runtime/query/iteration/phases/compaction.ts +53 -3
- package/src/runtime/query/iteration/phases/context.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +3 -3
- package/src/runtime/query/prelude-lease.ts +19 -0
- package/src/runtime/query/prepare-turn.ts +29 -12
- package/src/runtime/query/review-policy.ts +33 -0
- package/src/runtime/query/session-storage.ts +42 -3
- package/src/runtime/query/tooling.ts +3 -0
- package/src/runtime/query/turn/completion-audit.ts +76 -5
- package/src/runtime/query/turn/outcome-ledger.ts +633 -0
- package/src/sandbox/file-walk.ts +41 -4
- package/src/sandbox/glob-segment.ts +48 -24
- package/src/store/checkpoint/contract.ts +15 -0
- package/src/store/checkpoint/disk.ts +34 -2
- package/src/store/checkpoint/prune.ts +13 -0
- package/src/store/evidence/disk.ts +57 -2
- package/src/store/session-log/incremental.ts +147 -0
- package/src/store/session-log/index.ts +7 -0
- package/src/store/session-log/turn-outcome.ts +84 -0
- package/src/tools/admit-facts.ts +1 -0
- package/src/tools/builtins/bash.ts +12 -2
- package/src/tools/builtins/bounded-file-read.ts +59 -2
- package/src/tools/builtins/edit-apply.ts +26 -4
- package/src/tools/builtins/edit.ts +106 -41
- package/src/tools/builtins/glob.ts +1 -1
- package/src/tools/builtins/grep.ts +173 -27
- package/src/tools/builtins/notebook-guard.ts +76 -0
- package/src/tools/builtins/read-render.ts +12 -2
- package/src/tools/builtins/search-support.ts +62 -0
- package/src/tools/builtins/write-file.ts +12 -1
- package/src/tools/coordinator/agent.ts +31 -7
- package/src/tools/coordinator/index.ts +6 -1
- package/src/tools/coordinator/outcome-record.ts +93 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/tools/file-read-tracker.ts +51 -2
- package/src/tools/untrusted-envelope.ts +23 -1
- package/src/toolsets/manager.ts +6 -10
- package/src/toolsets/validation-issues.ts +97 -0
- package/src/turn/memory-recall.ts +28 -0
- package/src/types/agent/base.ts +20 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/common/index.ts +25 -1
- package/src/types/hitl/index.ts +27 -0
- package/src/types/session/config.ts +20 -0
- package/src/types/session/events.ts +3 -3
- package/src/types/session/index.ts +1 -0
- package/src/types/session/outcome.ts +139 -0
- package/src/types/session/prepare-step.ts +2 -0
- package/src/types/session/records.ts +67 -1
- package/src/types/session/turn.ts +19 -0
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +6 -0
package/src/sandbox/file-walk.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { VCS_NAMES, createIgnoreStack } from './ignore-matcher.js'
|
|
|
15
15
|
/** Entries a walk examines before it stops with what it has: a safety valve, not a budget. */
|
|
16
16
|
export const DEFAULT_MAX_VISITED_ENTRIES = 500_000
|
|
17
17
|
|
|
18
|
+
/** Longest the walk holds the event loop between yields. */
|
|
19
|
+
const WALK_SLICE_MS = 8
|
|
20
|
+
|
|
18
21
|
const GLOBSTAR = Symbol('globstar')
|
|
19
22
|
type Segment = string | { source: string; flags: string } | typeof GLOBSTAR
|
|
20
23
|
|
|
@@ -185,6 +188,7 @@ async function* executeWalk(
|
|
|
185
188
|
plan: WalkPlan,
|
|
186
189
|
signal?: AbortSignal,
|
|
187
190
|
onSummary?: (summary: SandboxWalkSummary) => void,
|
|
191
|
+
withoutSizes = false,
|
|
188
192
|
): AsyncGenerator<SandboxFileEntry> {
|
|
189
193
|
const fs = await import('node:fs/promises')
|
|
190
194
|
const path = await import('node:path')
|
|
@@ -196,6 +200,7 @@ async function* executeWalk(
|
|
|
196
200
|
/** Whether leaving this directory also drops its ignore layer. */
|
|
197
201
|
layered: boolean
|
|
198
202
|
}[] = []
|
|
203
|
+
let sliceStart = performance.now()
|
|
199
204
|
let visited = 0
|
|
200
205
|
let emitted = 0
|
|
201
206
|
let ignoredDirectories = 0
|
|
@@ -327,8 +332,22 @@ async function* executeWalk(
|
|
|
327
332
|
while (open.length > 0) {
|
|
328
333
|
check()
|
|
329
334
|
const frame = open[open.length - 1] as (typeof open)[number]
|
|
330
|
-
|
|
331
|
-
|
|
335
|
+
let entry: import('node:fs').Dirent | null
|
|
336
|
+
if (withoutSizes) {
|
|
337
|
+
// A local search consumer reads a directory's buffered entries
|
|
338
|
+
// synchronously: one promise per entry costs more than the listing.
|
|
339
|
+
// The loop yields on a time slice, and a blocked read cannot be
|
|
340
|
+
// raced, which only the default path promises.
|
|
341
|
+
if (performance.now() - sliceStart > WALK_SLICE_MS) {
|
|
342
|
+
await new Promise<void>((done) => setImmediate(done))
|
|
343
|
+
sliceStart = performance.now()
|
|
344
|
+
}
|
|
345
|
+
check()
|
|
346
|
+
entry = frame.dir.readSync()
|
|
347
|
+
} else {
|
|
348
|
+
entry = await wait(frame.dir.read())
|
|
349
|
+
check()
|
|
350
|
+
}
|
|
332
351
|
if (!entry) {
|
|
333
352
|
open.pop()
|
|
334
353
|
if (frame.layered) ignore?.leave()
|
|
@@ -351,6 +370,13 @@ async function* executeWalk(
|
|
|
351
370
|
}
|
|
352
371
|
if (!entry.isFile() || !matchers.some((matcher) => matcher.test(relative))) continue
|
|
353
372
|
if (ignore?.isIgnored(absolute, false)) continue
|
|
373
|
+
if (withoutSizes) {
|
|
374
|
+
// The caller opens the file anyway and reads its size from the handle.
|
|
375
|
+
emitted += 1
|
|
376
|
+
yield { path: absolute, size: 0 }
|
|
377
|
+
if (emitted >= plan.maxEntries) return
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
354
380
|
let info: import('node:fs').Stats
|
|
355
381
|
try {
|
|
356
382
|
info = await wait(fs.lstat(absolute))
|
|
@@ -384,10 +410,21 @@ function globalIgnoreFiles(): string[] {
|
|
|
384
410
|
/** Internal host path; the caller first resolves its authorized filesystem root. */
|
|
385
411
|
export async function* walkFilesLocally(
|
|
386
412
|
rootPath: string,
|
|
387
|
-
options: SandboxWalkFilesOptions
|
|
413
|
+
options: SandboxWalkFilesOptions & {
|
|
414
|
+
/**
|
|
415
|
+
* Skip the per-file size read: entries carry `size: 0`. For a caller that
|
|
416
|
+
* opens every file itself and takes the size from the handle.
|
|
417
|
+
*/
|
|
418
|
+
readonly withoutSizes?: boolean
|
|
419
|
+
},
|
|
388
420
|
): AsyncGenerator<SandboxFileEntry> {
|
|
389
421
|
options.signal?.throwIfAborted()
|
|
390
|
-
yield* executeWalk(
|
|
422
|
+
yield* executeWalk(
|
|
423
|
+
planWalk(rootPath, options),
|
|
424
|
+
options.signal,
|
|
425
|
+
options.onSummary,
|
|
426
|
+
options.withoutSizes === true,
|
|
427
|
+
)
|
|
391
428
|
}
|
|
392
429
|
|
|
393
430
|
/** The existing sandbox execution seam; no separate worker protocol is needed. */
|
|
@@ -7,22 +7,24 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Written for the `u` flag, which every compiled expression carries.
|
|
10
|
-
const POSIX_CLASSES:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
const POSIX_CLASSES: ReadonlyMap<string, string> = new Map(
|
|
11
|
+
Object.entries({
|
|
12
|
+
alnum: '\\p{L}\\p{Nl}\\p{Nd}',
|
|
13
|
+
alpha: '\\p{L}\\p{Nl}',
|
|
14
|
+
ascii: '\\x00-\\x7f',
|
|
15
|
+
blank: '\\p{Zs}\\t',
|
|
16
|
+
cntrl: '\\p{Cc}',
|
|
17
|
+
digit: '\\p{Nd}',
|
|
18
|
+
graph: '\\x21-\\x7e',
|
|
19
|
+
lower: '\\p{Ll}',
|
|
20
|
+
print: '\\x20-\\x7e',
|
|
21
|
+
punct: '\\p{P}\\p{S}',
|
|
22
|
+
space: '\\s',
|
|
23
|
+
upper: '\\p{Lu}',
|
|
24
|
+
word: '\\p{L}\\p{Nl}\\p{Nd}_',
|
|
25
|
+
xdigit: '0-9a-fA-F',
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
26
28
|
|
|
27
29
|
export interface CompiledSegment {
|
|
28
30
|
/** Set when the segment has no magic: the exact name it matches. */
|
|
@@ -31,6 +33,23 @@ export interface CompiledSegment {
|
|
|
31
33
|
source: string
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The longest segment, in UTF-16 code units, that `compileSegment` accepts. A
|
|
38
|
+
* file name is at most 255 bytes on the file systems Namzu runs on, so a
|
|
39
|
+
* pattern for one name beyond four times that is never legitimate, while
|
|
40
|
+
* compiling a segment costs time in proportion to the square of its length.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_GLOB_SEGMENT_LENGTH = 1024
|
|
43
|
+
|
|
44
|
+
/** How much of an over-long pattern an error message shows, in code points. */
|
|
45
|
+
const PREVIEW_LENGTH = 60
|
|
46
|
+
|
|
47
|
+
/** The first `PREVIEW_LENGTH` code points of `text`, never cutting a surrogate pair. */
|
|
48
|
+
const preview = (text: string) =>
|
|
49
|
+
Array.from(text.slice(0, PREVIEW_LENGTH * 2))
|
|
50
|
+
.slice(0, PREVIEW_LENGTH)
|
|
51
|
+
.join('')
|
|
52
|
+
|
|
34
53
|
/**
|
|
35
54
|
* Raised for a segment that repeats a group (`*( )` or `+( )`) which could match
|
|
36
55
|
* the same text more than one way. Such a group backtracks exponentially on a
|
|
@@ -38,14 +57,17 @@ export interface CompiledSegment {
|
|
|
38
57
|
*/
|
|
39
58
|
export class GlobTooComplexError extends Error {
|
|
40
59
|
readonly segment: string
|
|
41
|
-
constructor(segment: string, reason: 'ambiguous' | 'stretch' = 'ambiguous') {
|
|
60
|
+
constructor(segment: string, reason: 'ambiguous' | 'stretch' | 'length' = 'ambiguous') {
|
|
42
61
|
super(
|
|
43
|
-
reason === '
|
|
44
|
-
? `The pattern '${segment}'
|
|
45
|
-
:
|
|
62
|
+
reason === 'length'
|
|
63
|
+
? `The pattern '${preview(segment)}...' is too long (${segment.length} characters; at most ${MAX_GLOB_SEGMENT_LENGTH} between slashes); shorten it or split it across folders.`
|
|
64
|
+
: reason === 'stretch'
|
|
65
|
+
? `The pattern '${segment}' has more than three wildcards that can stretch; split it across folders or use fewer *.`
|
|
66
|
+
: `The pattern '${segment}' repeats a group that can match the same text more than one way; use ** or {a,b} instead.`,
|
|
46
67
|
)
|
|
47
68
|
this.name = 'GlobTooComplexError'
|
|
48
|
-
|
|
69
|
+
// An over-long pattern is not kept whole: the error may be logged.
|
|
70
|
+
this.segment = reason === 'length' ? preview(segment) : segment
|
|
49
71
|
}
|
|
50
72
|
}
|
|
51
73
|
|
|
@@ -85,7 +107,7 @@ function classEnd(text: string, start: number): number {
|
|
|
85
107
|
if (ch === '\\') index += 1
|
|
86
108
|
else if (ch === '[' && text[index + 1] === ':') {
|
|
87
109
|
const end = text.indexOf(':]', index + 2)
|
|
88
|
-
if (end > 0 && POSIX_CLASSES
|
|
110
|
+
if (end > 0 && POSIX_CLASSES.has(text.slice(index + 2, end))) index = end + 1
|
|
89
111
|
} else if (ch === ']') return index
|
|
90
112
|
}
|
|
91
113
|
return -1
|
|
@@ -134,8 +156,9 @@ function classSource(body: string): string {
|
|
|
134
156
|
if (ch === '[' && body[index + 1] === ':') {
|
|
135
157
|
const end = body.indexOf(':]', index + 2)
|
|
136
158
|
const name = end > 0 ? body.slice(index + 2, end) : ''
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
const members = POSIX_CLASSES.get(name)
|
|
160
|
+
if (members !== undefined) {
|
|
161
|
+
out += members
|
|
139
162
|
index = end + 1
|
|
140
163
|
continue
|
|
141
164
|
}
|
|
@@ -268,6 +291,7 @@ function isOneGroup(text: string): boolean {
|
|
|
268
291
|
* segment that itself begins with a literal `.` can.
|
|
269
292
|
*/
|
|
270
293
|
export function compileSegment(segment: string, dot: boolean): CompiledSegment {
|
|
294
|
+
if (segment.length > MAX_GLOB_SEGMENT_LENGTH) throw new GlobTooComplexError(segment, 'length')
|
|
271
295
|
const guard = dot ? '' : '(?!\\.)'
|
|
272
296
|
let parsed: Parsed
|
|
273
297
|
try {
|
|
@@ -62,6 +62,21 @@ export interface CheckpointLogView {
|
|
|
62
62
|
* `decision_resolved` or `decision_expired` yet. `prune` never deletes one.
|
|
63
63
|
*/
|
|
64
64
|
openDecisionCheckpoints(scope: CheckpointScope): Promise<Iterable<CheckpointId>>
|
|
65
|
+
/**
|
|
66
|
+
* Both answers `prune` needs in one read of the log: for each of
|
|
67
|
+
* `checkpointIds`, what `writtenDocSha256` would return (absent from
|
|
68
|
+
* `written` when there is no record), and what `openDecisionCheckpoints`
|
|
69
|
+
* would return. Optional: a view without it is asked the two questions
|
|
70
|
+
* separately, one walk of the log each, which costs a walk per stored
|
|
71
|
+
* checkpoint on every prune.
|
|
72
|
+
*/
|
|
73
|
+
pruneFacts?(
|
|
74
|
+
scope: CheckpointScope,
|
|
75
|
+
checkpointIds: readonly CheckpointId[],
|
|
76
|
+
): Promise<{
|
|
77
|
+
readonly written: ReadonlyMap<CheckpointId, string>
|
|
78
|
+
readonly open: Iterable<CheckpointId>
|
|
79
|
+
}>
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
/** Why a restore refused a checkpoint. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { link, mkdir, open, readFile, readdir, unlink } from 'node:fs/promises'
|
|
1
|
+
import { link, mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises'
|
|
2
2
|
import type { FileHandle } from 'node:fs/promises'
|
|
3
3
|
import { dirname } from 'node:path'
|
|
4
4
|
import type { SessionLocator, SessionPaths } from '../../session/paths.js'
|
|
@@ -38,6 +38,17 @@ export interface DiskSessionCheckpointStoreOptions {
|
|
|
38
38
|
|
|
39
39
|
const DOCUMENT = /^(.+)\.json$/
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Which turn a checkpoint document on disk belongs to, as last read, with the
|
|
43
|
+
* size and mtime it had then. A session keeps its checkpoints for every turn
|
|
44
|
+
* in one directory, so listing one turn's used to read and parse every
|
|
45
|
+
* document of the session, and the directory only grows. A document is not
|
|
46
|
+
* read again while a `stat` says it is the one that was; one that belonged to
|
|
47
|
+
* another turn is skipped on that evidence alone.
|
|
48
|
+
*/
|
|
49
|
+
const documentTurns = new Map<string, { turnId: string; size: number; mtimeMs: number }>()
|
|
50
|
+
const DOCUMENT_TURNS_KEPT = 20_000
|
|
51
|
+
|
|
41
52
|
/**
|
|
42
53
|
* {@link SessionCheckpointStore} at `<session-dir>/checkpoints/<id>.json`.
|
|
43
54
|
*
|
|
@@ -145,10 +156,31 @@ export class DiskSessionCheckpointStore implements SessionCheckpointStore {
|
|
|
145
156
|
// Sidecars (`<id>.json.<pid>.<n>.<hex>.tmp`) and strangers are not documents.
|
|
146
157
|
const id = DOCUMENT.exec(name)?.[1]
|
|
147
158
|
if (id === undefined || !isEntityId(id, 'checkpoint')) continue
|
|
148
|
-
const
|
|
159
|
+
const path = this.#paths.checkpointFile(locator, id)
|
|
160
|
+
const seen = await stat(path).catch((error: NodeJS.ErrnoException) => {
|
|
161
|
+
if (error.code === 'ENOENT') return null
|
|
162
|
+
throw error
|
|
163
|
+
})
|
|
149
164
|
// Deleted between the listing and the read: a concurrent prune, not damage.
|
|
165
|
+
if (seen === null) continue
|
|
166
|
+
const known = documentTurns.get(path)
|
|
167
|
+
if (
|
|
168
|
+
known !== undefined &&
|
|
169
|
+
known.turnId !== checked.turnId &&
|
|
170
|
+
known.size === seen.size &&
|
|
171
|
+
known.mtimeMs === seen.mtimeMs
|
|
172
|
+
) {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
const text = await readIfPresent(path)
|
|
150
176
|
if (text === null) continue
|
|
151
177
|
const checkpoint = parseStoredCheckpoint(text, checked, id)
|
|
178
|
+
if (documentTurns.size >= DOCUMENT_TURNS_KEPT) documentTurns.clear()
|
|
179
|
+
documentTurns.set(path, {
|
|
180
|
+
turnId: checkpoint.turnId,
|
|
181
|
+
size: seen.size,
|
|
182
|
+
mtimeMs: seen.mtimeMs,
|
|
183
|
+
})
|
|
152
184
|
if (checkpoint.turnId === checked.turnId) checkpoints.push(checkpoint)
|
|
153
185
|
}
|
|
154
186
|
return checkpoints.sort(compareCheckpoints)
|
|
@@ -79,6 +79,19 @@ export async function selectStoredCheckpointsToPrune(
|
|
|
79
79
|
if (!Number.isSafeInteger(keepLast) || keepLast < 0) {
|
|
80
80
|
throw new RangeError(`keepLast must be a nonnegative integer, got ${String(keepLast)}`)
|
|
81
81
|
}
|
|
82
|
+
if (log.pruneFacts) {
|
|
83
|
+
// One read of the log answers both questions for every stored
|
|
84
|
+
// checkpoint, instead of one full walk each.
|
|
85
|
+
const facts = await log.pruneFacts(
|
|
86
|
+
scope,
|
|
87
|
+
stored.map((checkpoint) => checkpoint.checkpointId),
|
|
88
|
+
)
|
|
89
|
+
return selectSessionCheckpointsToPrune(
|
|
90
|
+
stored.filter((checkpoint) => facts.written.has(checkpoint.checkpointId)),
|
|
91
|
+
keepLast,
|
|
92
|
+
new Set(facts.open),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
82
95
|
const committed: Checkpoint[] = []
|
|
83
96
|
for (const checkpoint of stored) {
|
|
84
97
|
if ((await log.writtenDocSha256(scope, checkpoint.checkpointId)) !== null) {
|
|
@@ -157,22 +157,64 @@ export function createSessionEvidenceSource(
|
|
|
157
157
|
})
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/** What a scan of a session log's complete records, from byte 0 up to `consumed`, found. */
|
|
161
|
+
interface SessionScan {
|
|
162
|
+
readonly identity: string
|
|
163
|
+
/** Offset just past the last complete record scanned. */
|
|
164
|
+
readonly consumed: number
|
|
165
|
+
readonly first: Buffer
|
|
166
|
+
readonly active: string | undefined
|
|
167
|
+
readonly turnClosed: boolean
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Scans already done, by log file and scope, so that an access reads only the
|
|
172
|
+
* records appended since the last one instead of parsing the whole log again.
|
|
173
|
+
* A session log only grows, and every search page makes several accesses.
|
|
174
|
+
*/
|
|
175
|
+
const SCAN_CACHE_LIMIT = 64
|
|
176
|
+
const scanCache = new Map<string, SessionScan>()
|
|
177
|
+
|
|
178
|
+
function remember(key: string, scan: SessionScan): void {
|
|
179
|
+
scanCache.delete(key)
|
|
180
|
+
scanCache.set(key, scan)
|
|
181
|
+
if (scanCache.size > SCAN_CACHE_LIMIT) {
|
|
182
|
+
const oldest = scanCache.keys().next().value
|
|
183
|
+
if (oldest !== undefined) scanCache.delete(oldest)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
160
187
|
/**
|
|
161
188
|
* The first record of a session log (its `session_started`) and whether the
|
|
162
189
|
* scope is closed: no turn of the session active, or — for a scope narrowed
|
|
163
190
|
* to one turn — that turn settled.
|
|
191
|
+
*
|
|
192
|
+
* The scan resumes from the one a previous access left in `scanCache` when it
|
|
193
|
+
* is of the same file (same identity, first record unchanged on disk) and
|
|
194
|
+
* ended at or before `size`; otherwise it starts again from byte 0.
|
|
164
195
|
*/
|
|
165
196
|
async function sessionState(
|
|
166
197
|
handle: FileHandle,
|
|
167
198
|
size: number,
|
|
168
199
|
scope: z.infer<typeof scopeSchema>,
|
|
200
|
+
cacheKey: string,
|
|
201
|
+
identity: string,
|
|
169
202
|
): Promise<{ first: Buffer; closed: boolean }> {
|
|
170
203
|
const unmetered: EvidenceBudget = { bytes: 0, limit: Number.MAX_SAFE_INTEGER }
|
|
171
204
|
let first: Buffer | undefined
|
|
172
205
|
let active: string | undefined
|
|
173
206
|
let turnClosed = false
|
|
174
|
-
let buffered = Buffer.alloc(0)
|
|
175
207
|
let offset = 0
|
|
208
|
+
const cached = scanCache.get(cacheKey)
|
|
209
|
+
if (cached && cached.identity === identity && cached.consumed <= size) {
|
|
210
|
+
const head = await readBytes(handle, 0, cached.first.length, unmetered)
|
|
211
|
+
if (head.equals(cached.first)) {
|
|
212
|
+
;({ first, active, turnClosed } = cached)
|
|
213
|
+
offset = cached.consumed
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
let consumed = offset
|
|
217
|
+
let buffered = Buffer.alloc(0)
|
|
176
218
|
while (offset < size || buffered.length > 0) {
|
|
177
219
|
let newline = buffered.indexOf(10)
|
|
178
220
|
if (newline < 0) {
|
|
@@ -186,6 +228,7 @@ async function sessionState(
|
|
|
186
228
|
const raw = buffered.subarray(0, newline + 1)
|
|
187
229
|
buffered = buffered.subarray(newline + 1)
|
|
188
230
|
newline = -1
|
|
231
|
+
consumed += raw.length
|
|
189
232
|
const record = JSON.parse(decode(raw)) as Record<string, unknown>
|
|
190
233
|
if (!first) {
|
|
191
234
|
if (
|
|
@@ -206,6 +249,12 @@ async function sessionState(
|
|
|
206
249
|
}
|
|
207
250
|
}
|
|
208
251
|
if (!first) throw new Error('The session log is empty; evidence is incomplete.')
|
|
252
|
+
// A source anchored earlier in the log scans less than another access has;
|
|
253
|
+
// keep the longer scan for the next one.
|
|
254
|
+
const kept = scanCache.get(cacheKey)
|
|
255
|
+
if (!kept || kept.identity !== identity || kept.consumed <= consumed) {
|
|
256
|
+
remember(cacheKey, { identity, consumed, first, active, turnClosed })
|
|
257
|
+
}
|
|
209
258
|
return { first, closed: scope.turnId !== undefined ? turnClosed : active === undefined }
|
|
210
259
|
}
|
|
211
260
|
|
|
@@ -258,7 +307,13 @@ function createSource(
|
|
|
258
307
|
? await completePrefix(handle, before.size, budget)
|
|
259
308
|
: before.size
|
|
260
309
|
}
|
|
261
|
-
const state = await sessionState(
|
|
310
|
+
const state = await sessionState(
|
|
311
|
+
handle,
|
|
312
|
+
size,
|
|
313
|
+
scope,
|
|
314
|
+
`${logPath}\0${scopeKey}`,
|
|
315
|
+
fileIdentity(before),
|
|
316
|
+
)
|
|
262
317
|
if (!state.closed && consistency === 'closed') {
|
|
263
318
|
throw new Error('Evidence scope has an active turn; it is not closed.')
|
|
264
319
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { RecordPointer } from '../../types/session/records.js'
|
|
2
|
+
import { type SessionLogEntry, SessionLogIntegrityError } from './chain.js'
|
|
3
|
+
import type { SessionLog } from './core.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reductions over a whole session log, advanced rather than recomputed.
|
|
7
|
+
*
|
|
8
|
+
* A session log only grows, and every turn asks it the same questions (which
|
|
9
|
+
* checkpoints were committed, which turn ids exist, what the folded context
|
|
10
|
+
* is). Answering each from byte 0 parses and verifies every record the
|
|
11
|
+
* session ever wrote, so a turn cost more the longer the session. A reduction
|
|
12
|
+
* held here remembers the last record it folded and, on the next ask, walks
|
|
13
|
+
* only what was appended after it. `SessionLog.read({ after })` checks that
|
|
14
|
+
* record against the log byte for byte before continuing, so a log that was
|
|
15
|
+
* replaced, truncated or rewritten at that record is noticed and the
|
|
16
|
+
* reduction starts again from byte 0 with the original behaviour (strict reads
|
|
17
|
+
* refuse a broken chain, tolerant reads stop at the break).
|
|
18
|
+
*
|
|
19
|
+
* What a resumed read does not do is re-verify the records before the anchor:
|
|
20
|
+
* they were verified when they were first folded, by this process. A byte
|
|
21
|
+
* flipped in them afterwards, with every later record left as it was, is not
|
|
22
|
+
* noticed by this path; a fresh process (a resume) always starts from byte 0.
|
|
23
|
+
*
|
|
24
|
+
* The cache is keyed by session id and the reduction's own name and mode,
|
|
25
|
+
* holds a few sessions, and is anchored by content, so two logs that share a
|
|
26
|
+
* session id never share a result.
|
|
27
|
+
*
|
|
28
|
+
* Cost that remains: the state of the "every message ever recorded" reduction
|
|
29
|
+
* holds each message the log ever recorded, superseded and compacted ones
|
|
30
|
+
* included, so it grows with the log and not with the live context, and it
|
|
31
|
+
* lives for the life of the process (for the sessions kept). Reading it
|
|
32
|
+
* returns a copy of all of them, which is O(history) per turn. Both are small
|
|
33
|
+
* next to the whole-log walks this file removes, and are left as they are.
|
|
34
|
+
* More than SESSIONS_KEPT sessions in use at once evict one another, and each
|
|
35
|
+
* eviction restarts from byte 0, the cost before this file existed.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const SESSIONS_KEPT = 4
|
|
39
|
+
|
|
40
|
+
interface Kept {
|
|
41
|
+
state: unknown
|
|
42
|
+
/** The last record folded; the next read resumes after it. */
|
|
43
|
+
anchor: RecordPointer | undefined
|
|
44
|
+
/** Calls on one reduction take turns: the state is advanced in place. */
|
|
45
|
+
turn: Promise<unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sessions = new Map<string, Map<string, Kept>>()
|
|
49
|
+
|
|
50
|
+
function keptFor(sessionId: string, key: string): Kept {
|
|
51
|
+
let reductions = sessions.get(sessionId)
|
|
52
|
+
if (reductions === undefined) {
|
|
53
|
+
reductions = new Map()
|
|
54
|
+
if (sessions.size >= SESSIONS_KEPT) {
|
|
55
|
+
const oldest = sessions.keys().next().value
|
|
56
|
+
if (oldest !== undefined) sessions.delete(oldest)
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
sessions.delete(sessionId)
|
|
60
|
+
}
|
|
61
|
+
sessions.set(sessionId, reductions)
|
|
62
|
+
let kept = reductions.get(key)
|
|
63
|
+
if (kept === undefined) {
|
|
64
|
+
kept = { state: undefined, anchor: undefined, turn: Promise.resolve() }
|
|
65
|
+
reductions.set(key, kept)
|
|
66
|
+
}
|
|
67
|
+
return kept
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IncrementalFold<S, R> {
|
|
71
|
+
/** The reduction's name; one cache slot per name and read mode. */
|
|
72
|
+
readonly name: string
|
|
73
|
+
readonly mode: 'strict' | 'tolerant'
|
|
74
|
+
/** A fresh, empty state. */
|
|
75
|
+
create(): S
|
|
76
|
+
/** Fold one record into the state, in place. */
|
|
77
|
+
apply(state: S, entry: SessionLogEntry): void
|
|
78
|
+
/**
|
|
79
|
+
* The answer, from a state that is current for as long as this call runs.
|
|
80
|
+
* It must not keep or hand out the state itself, which later calls advance.
|
|
81
|
+
*/
|
|
82
|
+
result(state: S): Promise<R> | R
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fold `log` into `fold.create()` through `fold.apply`, resuming from the
|
|
87
|
+
* record the previous call for this session stopped at when the log still
|
|
88
|
+
* holds it, and answer with `fold.result`.
|
|
89
|
+
*/
|
|
90
|
+
export async function foldSessionLog<S, R>(
|
|
91
|
+
log: SessionLog,
|
|
92
|
+
fold: IncrementalFold<S, R>,
|
|
93
|
+
): Promise<R> {
|
|
94
|
+
const kept = keptFor(log.sessionId, `${fold.name}:${fold.mode}`)
|
|
95
|
+
const run = async (): Promise<R> => {
|
|
96
|
+
let state = kept.state as S | undefined
|
|
97
|
+
let anchor = kept.anchor
|
|
98
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
99
|
+
const resuming = state !== undefined && anchor !== undefined
|
|
100
|
+
if (!resuming) {
|
|
101
|
+
state = fold.create()
|
|
102
|
+
anchor = undefined
|
|
103
|
+
}
|
|
104
|
+
let moved = false
|
|
105
|
+
try {
|
|
106
|
+
const walk = log.read({ mode: fold.mode, ...(anchor ? { after: anchor } : {}) })
|
|
107
|
+
let summary: Awaited<ReturnType<typeof walk.next>>
|
|
108
|
+
for (;;) {
|
|
109
|
+
summary = await walk.next()
|
|
110
|
+
if (summary.done) break
|
|
111
|
+
moved = true
|
|
112
|
+
fold.apply(state as S, summary.value)
|
|
113
|
+
}
|
|
114
|
+
const ended = summary.value
|
|
115
|
+
if (resuming && !moved && ended.break?.reason === 'anchor-mismatch') {
|
|
116
|
+
kept.state = undefined
|
|
117
|
+
kept.anchor = undefined
|
|
118
|
+
state = undefined
|
|
119
|
+
anchor = undefined
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
kept.state = state
|
|
123
|
+
kept.anchor = ended.head?.pointer ?? anchor
|
|
124
|
+
return await fold.result(state as S)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// The state may be half-advanced: never reuse it.
|
|
127
|
+
kept.state = undefined
|
|
128
|
+
kept.anchor = undefined
|
|
129
|
+
if (
|
|
130
|
+
resuming &&
|
|
131
|
+
!moved &&
|
|
132
|
+
error instanceof SessionLogIntegrityError &&
|
|
133
|
+
error.reason === 'anchor-mismatch'
|
|
134
|
+
) {
|
|
135
|
+
state = undefined
|
|
136
|
+
anchor = undefined
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
throw error
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new Error('unreachable')
|
|
143
|
+
}
|
|
144
|
+
const result = kept.turn.then(run, run)
|
|
145
|
+
kept.turn = result.catch(() => undefined)
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
@@ -61,6 +61,13 @@ export {
|
|
|
61
61
|
withMessageId,
|
|
62
62
|
} from './fold.js'
|
|
63
63
|
export { type TornTailRepair, repairRecordDraft } from './heal.js'
|
|
64
|
+
export {
|
|
65
|
+
foldChildOutcomes,
|
|
66
|
+
foldTurnOutcomes,
|
|
67
|
+
parseTurnOutcome,
|
|
68
|
+
readOutcomeProgress,
|
|
69
|
+
readTurnOutcome,
|
|
70
|
+
} from './turn-outcome.js'
|
|
64
71
|
export {
|
|
65
72
|
type ReleaseHeldSessionLeasesOptions,
|
|
66
73
|
type ReleaseHeldSessionLeasesResult,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CheckpointId, SessionId, TurnId } from '../../types/ids/index.js'
|
|
2
|
+
import { type TurnOutcome, TurnOutcomeSchema } from '../../types/session/outcome.js'
|
|
3
|
+
import type { SessionRecord } from '../../types/session/records.js'
|
|
4
|
+
import type { SessionLog } from './core.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reading the turn's outcome record back out of the session log.
|
|
8
|
+
*
|
|
9
|
+
* It lives in three records of its own (`turn_outcome_recorded`,
|
|
10
|
+
* `child_outcome_recorded`, `outcome_progress_written`), each flagged
|
|
11
|
+
* `ignorable`, and never in a field of an older record: a release that
|
|
12
|
+
* validates `turn_completed`, `child_session_ended` or the checkpoint strictly
|
|
13
|
+
* reads a log that has them and loses only the outcome. The fold of messages
|
|
14
|
+
* does not look at them. A body that does not parse as a `TurnOutcome` reads
|
|
15
|
+
* as no outcome, never as a guess.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `value` as a {@link TurnOutcome}, or `undefined` when it is not one. */
|
|
19
|
+
export function parseTurnOutcome(value: unknown): TurnOutcome | undefined {
|
|
20
|
+
const parsed = TurnOutcomeSchema.safeParse(value)
|
|
21
|
+
return parsed.success ? parsed.data : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The outcome of each turn that recorded one, from records in log order. A
|
|
26
|
+
* turn that recorded none is absent, so a caller never reads `unchanged` into
|
|
27
|
+
* a turn nobody measured.
|
|
28
|
+
*/
|
|
29
|
+
export function foldTurnOutcomes(records: Iterable<SessionRecord>): Map<TurnId, TurnOutcome> {
|
|
30
|
+
const outcomes = new Map<TurnId, TurnOutcome>()
|
|
31
|
+
for (const record of records) {
|
|
32
|
+
if (record.type !== 'turn_outcome_recorded') continue
|
|
33
|
+
const outcome = parseTurnOutcome(record.outcome)
|
|
34
|
+
if (outcome) outcomes.set(record.turnId, outcome)
|
|
35
|
+
else outcomes.delete(record.turnId)
|
|
36
|
+
}
|
|
37
|
+
return outcomes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The outcome `turnId` recorded, or `undefined`. */
|
|
41
|
+
export async function readTurnOutcome(
|
|
42
|
+
log: SessionLog,
|
|
43
|
+
turnId: TurnId,
|
|
44
|
+
): Promise<TurnOutcome | undefined> {
|
|
45
|
+
let found: TurnOutcome | undefined
|
|
46
|
+
for await (const { record } of log.read({ mode: 'tolerant' })) {
|
|
47
|
+
if (record.type !== 'turn_outcome_recorded' || record.turnId !== turnId) continue
|
|
48
|
+
found = parseTurnOutcome(record.outcome)
|
|
49
|
+
}
|
|
50
|
+
return found
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The outcome a child's settled turn recorded in the parent's log, by child
|
|
55
|
+
* session id. A child with no record is absent.
|
|
56
|
+
*/
|
|
57
|
+
export function foldChildOutcomes(records: Iterable<SessionRecord>): Map<SessionId, TurnOutcome> {
|
|
58
|
+
const outcomes = new Map<SessionId, TurnOutcome>()
|
|
59
|
+
for (const record of records) {
|
|
60
|
+
if (record.type !== 'child_outcome_recorded') continue
|
|
61
|
+
const outcome = parseTurnOutcome(record.outcome)
|
|
62
|
+
if (outcome) outcomes.set(record.childSessionId, outcome)
|
|
63
|
+
else outcomes.delete(record.childSessionId)
|
|
64
|
+
}
|
|
65
|
+
return outcomes
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What the outcome record had gathered when `checkpointId` was written, or
|
|
70
|
+
* `undefined` when no progress record names it (a checkpoint written before
|
|
71
|
+
* the record existed, or by a release that does not write it).
|
|
72
|
+
*/
|
|
73
|
+
export async function readOutcomeProgress(
|
|
74
|
+
log: SessionLog,
|
|
75
|
+
checkpointId: CheckpointId,
|
|
76
|
+
): Promise<Readonly<Record<string, unknown>> | undefined> {
|
|
77
|
+
let found: Readonly<Record<string, unknown>> | undefined
|
|
78
|
+
for await (const { record } of log.read({ mode: 'tolerant' })) {
|
|
79
|
+
if (record.type === 'outcome_progress_written' && record.checkpointId === checkpointId) {
|
|
80
|
+
found = record.progress
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return found
|
|
84
|
+
}
|