@namzu/sdk 59.0.0 → 60.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.
- package/CHANGELOG.md +102 -0
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +77 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +4 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +90 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +31 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -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/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/records.d.ts +264 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +32 -1
- package/dist/types/session/records.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 +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/compaction/context-window.ts +163 -2
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/model-window-suffix.ts +84 -0
- package/src/public-runtime.ts +21 -2
- package/src/public-types.ts +1 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +92 -10
- package/src/runtime/query/index.ts +50 -7
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +7 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/plugin/index.ts +8 -0
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/records.ts +36 -2
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +8 -0
package/src/sandbox/file-walk.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAbsolute, posix } from 'node:path'
|
|
1
|
+
import { isAbsolute, join, posix } from 'node:path'
|
|
2
2
|
import braceExpansion from 'brace-expansion'
|
|
3
3
|
import { GLOBSTAR, Minimatch } from 'minimatch'
|
|
4
4
|
import type {
|
|
@@ -6,9 +6,14 @@ import type {
|
|
|
6
6
|
SandboxExecResult,
|
|
7
7
|
SandboxFileEntry,
|
|
8
8
|
SandboxWalkFilesOptions,
|
|
9
|
+
SandboxWalkSummary,
|
|
9
10
|
} from '../types/sandbox/index.js'
|
|
10
11
|
import { subscribeToAbort } from '../utils/abort.js'
|
|
11
12
|
import { FILE_WALK_PROGRAM } from './file-walk-program.js'
|
|
13
|
+
import { VCS_NAMES, createIgnoreStack } from './ignore-matcher.js'
|
|
14
|
+
|
|
15
|
+
/** Entries a walk examines before it stops with what it has: a safety valve, not a budget. */
|
|
16
|
+
export const DEFAULT_MAX_VISITED_ENTRIES = 500_000
|
|
12
17
|
|
|
13
18
|
interface WalkPlan {
|
|
14
19
|
root: string
|
|
@@ -18,6 +23,8 @@ interface WalkPlan {
|
|
|
18
23
|
maxEntries: number
|
|
19
24
|
maxVisitedEntries: number
|
|
20
25
|
includeHidden: boolean
|
|
26
|
+
respectIgnore: boolean
|
|
27
|
+
failOnLimit: boolean
|
|
21
28
|
patterns: (string | { source: string; flags: string } | null)[][]
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -103,8 +110,13 @@ function planWalk(root: string, options: SandboxWalkFilesOptions): WalkPlan {
|
|
|
103
110
|
expressions,
|
|
104
111
|
maxDepth: Number.isFinite(maxDepth) ? maxDepth : null,
|
|
105
112
|
maxEntries: positiveInteger(options.maxEntries, 'maxEntries'),
|
|
106
|
-
maxVisitedEntries: positiveInteger(
|
|
113
|
+
maxVisitedEntries: positiveInteger(
|
|
114
|
+
options.maxVisitedEntries ?? DEFAULT_MAX_VISITED_ENTRIES,
|
|
115
|
+
'maxVisitedEntries',
|
|
116
|
+
),
|
|
107
117
|
includeHidden: options.includeHidden ?? false,
|
|
118
|
+
respectIgnore: options.respectIgnore ?? true,
|
|
119
|
+
failOnLimit: options.failOnLimit ?? false,
|
|
108
120
|
patterns: sets.map((set) =>
|
|
109
121
|
set.map((segment) =>
|
|
110
122
|
segment === GLOBSTAR
|
|
@@ -151,13 +163,25 @@ function directoryMatcher(plan: WalkPlan): (relative: string) => boolean {
|
|
|
151
163
|
async function* executeWalk(
|
|
152
164
|
plan: WalkPlan,
|
|
153
165
|
signal?: AbortSignal,
|
|
166
|
+
onSummary?: (summary: SandboxWalkSummary) => void,
|
|
154
167
|
): AsyncGenerator<SandboxFileEntry> {
|
|
155
168
|
const fs = await import('node:fs/promises')
|
|
156
169
|
const path = await import('node:path')
|
|
157
170
|
const { EventEmitter } = await import('node:events')
|
|
158
|
-
const open: {
|
|
171
|
+
const open: {
|
|
172
|
+
dir: import('node:fs').Dir
|
|
173
|
+
relative: string
|
|
174
|
+
depth: number
|
|
175
|
+
/** Whether leaving this directory also drops its ignore layer. */
|
|
176
|
+
layered: boolean
|
|
177
|
+
}[] = []
|
|
159
178
|
let visited = 0
|
|
160
179
|
let emitted = 0
|
|
180
|
+
let ignoredDirectories = 0
|
|
181
|
+
let stopped: SandboxWalkSummary['stopped']
|
|
182
|
+
const ignore = plan.respectIgnore
|
|
183
|
+
? createIgnoreStack({ fs, path, extraFiles: globalIgnoreFiles() })
|
|
184
|
+
: undefined
|
|
161
185
|
const matchers = plan.expressions.map(({ source, flags }) => new RegExp(source, `${flags}s`))
|
|
162
186
|
const canDescend = directoryMatcher(plan)
|
|
163
187
|
const maxDepth = plan.maxDepth ?? Number.POSITIVE_INFINITY
|
|
@@ -204,9 +228,11 @@ async function* executeWalk(
|
|
|
204
228
|
dispose?.()
|
|
205
229
|
}
|
|
206
230
|
}
|
|
231
|
+
/** False once the safety valve trips; the walk then ends with what it has. */
|
|
207
232
|
const noteVisit = () => {
|
|
208
233
|
visited += 1
|
|
209
|
-
if (visited
|
|
234
|
+
if (visited <= plan.maxVisitedEntries) return true
|
|
235
|
+
if (plan.failOnLimit) {
|
|
210
236
|
throw Object.assign(
|
|
211
237
|
new Error(
|
|
212
238
|
`File search stopped after examining ${plan.maxVisitedEntries} entries; narrow its root or pattern.`,
|
|
@@ -214,6 +240,8 @@ async function* executeWalk(
|
|
|
214
240
|
{ code: 'ERR_FILE_WALK_LIMIT' },
|
|
215
241
|
)
|
|
216
242
|
}
|
|
243
|
+
stopped = 'entry-limit'
|
|
244
|
+
return false
|
|
217
245
|
}
|
|
218
246
|
try {
|
|
219
247
|
check()
|
|
@@ -231,11 +259,14 @@ async function* executeWalk(
|
|
|
231
259
|
const rootInfo = await wait(fs.lstat(plan.root))
|
|
232
260
|
check()
|
|
233
261
|
if (rootInfo.isFile()) {
|
|
234
|
-
noteVisit()
|
|
262
|
+
if (!noteVisit()) return
|
|
235
263
|
if (matchers.some((matcher) => matcher.test(path.basename(plan.root))))
|
|
236
264
|
yield { path: plan.root, size: rootInfo.size }
|
|
237
265
|
return
|
|
238
266
|
}
|
|
267
|
+
// An explicit root is never itself ignored: only what is below it is tested.
|
|
268
|
+
await ignore?.init(plan.root)
|
|
269
|
+
await ignore?.enter(plan.root)
|
|
239
270
|
let start = plan.root
|
|
240
271
|
// Never follow a static-prefix symlink to speed up a pattern: traversal
|
|
241
272
|
// and its optimized starting point must have the same link policy.
|
|
@@ -249,22 +280,29 @@ async function* executeWalk(
|
|
|
249
280
|
if (missing(error)) return
|
|
250
281
|
throw error
|
|
251
282
|
}
|
|
252
|
-
noteVisit()
|
|
283
|
+
if (!noteVisit()) return
|
|
253
284
|
if (!info.isDirectory()) return
|
|
285
|
+
await ignore?.enter(start)
|
|
254
286
|
}
|
|
255
287
|
if (plan.prefix.length >= maxDepth) return
|
|
256
|
-
const openDirectory = async (
|
|
288
|
+
const openDirectory = async (
|
|
289
|
+
absolute: string,
|
|
290
|
+
relative: string,
|
|
291
|
+
depth: number,
|
|
292
|
+
layered: boolean,
|
|
293
|
+
) => {
|
|
257
294
|
check()
|
|
258
295
|
try {
|
|
259
296
|
const dir = await wait(fs.opendir(absolute), (late) => {
|
|
260
297
|
void close(late).catch(() => {})
|
|
261
298
|
})
|
|
262
|
-
|
|
299
|
+
if (layered) await ignore?.enter(absolute)
|
|
300
|
+
open.push({ dir, relative, depth, layered })
|
|
263
301
|
} catch (error) {
|
|
264
302
|
if (!missing(error)) throw error
|
|
265
303
|
}
|
|
266
304
|
}
|
|
267
|
-
await openDirectory(start, plan.prefix.join('/'), plan.prefix.length)
|
|
305
|
+
await openDirectory(start, plan.prefix.join('/'), plan.prefix.length, false)
|
|
268
306
|
while (open.length > 0) {
|
|
269
307
|
check()
|
|
270
308
|
const frame = open[open.length - 1] as (typeof open)[number]
|
|
@@ -272,18 +310,26 @@ async function* executeWalk(
|
|
|
272
310
|
check()
|
|
273
311
|
if (!entry) {
|
|
274
312
|
open.pop()
|
|
313
|
+
if (frame.layered) ignore?.leave()
|
|
275
314
|
await close(frame.dir)
|
|
276
315
|
continue
|
|
277
316
|
}
|
|
278
|
-
noteVisit()
|
|
317
|
+
if (!noteVisit()) return
|
|
318
|
+
if (VCS_NAMES.has(entry.name)) continue
|
|
279
319
|
const relative = frame.relative ? `${frame.relative}/${entry.name}` : entry.name
|
|
280
320
|
const absolute = path.join(plan.root, relative)
|
|
281
321
|
const depth = frame.depth + 1
|
|
282
322
|
if (entry.isDirectory()) {
|
|
283
|
-
if (depth
|
|
323
|
+
if (depth >= maxDepth || !canDescend(relative)) continue
|
|
324
|
+
if (ignore?.isIgnored(absolute, true)) {
|
|
325
|
+
ignoredDirectories += 1
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
await openDirectory(absolute, relative, depth, true)
|
|
284
329
|
continue
|
|
285
330
|
}
|
|
286
331
|
if (!entry.isFile() || !matchers.some((matcher) => matcher.test(relative))) continue
|
|
332
|
+
if (ignore?.isIgnored(absolute, false)) continue
|
|
287
333
|
let info: import('node:fs').Stats
|
|
288
334
|
try {
|
|
289
335
|
info = await wait(fs.lstat(absolute))
|
|
@@ -303,16 +349,24 @@ async function* executeWalk(
|
|
|
303
349
|
void close(dir).catch(() => {})
|
|
304
350
|
} else await close(dir)
|
|
305
351
|
}
|
|
352
|
+
onSummary?.({ visited, ignoredDirectories, ...(stopped ? { stopped } : {}) })
|
|
306
353
|
}
|
|
307
354
|
}
|
|
308
355
|
|
|
356
|
+
/** Global exclude files, lowest precedence. Only the host has them. */
|
|
357
|
+
function globalIgnoreFiles(): string[] {
|
|
358
|
+
const home = process.env.HOME ?? process.env.USERPROFILE
|
|
359
|
+
const base = process.env.XDG_CONFIG_HOME || (home ? join(home, '.config') : undefined)
|
|
360
|
+
return base ? [join(base, 'git', 'ignore')] : []
|
|
361
|
+
}
|
|
362
|
+
|
|
309
363
|
/** Internal host path; the caller first resolves its authorized filesystem root. */
|
|
310
364
|
export async function* walkFilesLocally(
|
|
311
365
|
rootPath: string,
|
|
312
366
|
options: SandboxWalkFilesOptions,
|
|
313
367
|
): AsyncGenerator<SandboxFileEntry> {
|
|
314
368
|
options.signal?.throwIfAborted()
|
|
315
|
-
yield* executeWalk(planWalk(rootPath, options), options.signal)
|
|
369
|
+
yield* executeWalk(planWalk(rootPath, options), options.signal, options.onSummary)
|
|
316
370
|
}
|
|
317
371
|
|
|
318
372
|
/** The existing sandbox execution seam; no separate worker protocol is needed. */
|
|
@@ -346,6 +400,7 @@ export async function* walkFilesViaExec(
|
|
|
346
400
|
let terminal = false
|
|
347
401
|
let received = 0
|
|
348
402
|
let failure: unknown
|
|
403
|
+
const outcome: { summary?: SandboxWalkSummary } = {}
|
|
349
404
|
const reject = (error: unknown) => {
|
|
350
405
|
failure ??= error instanceof Error ? error : new Error(String(error))
|
|
351
406
|
controller.abort(error)
|
|
@@ -358,6 +413,13 @@ export async function* walkFilesViaExec(
|
|
|
358
413
|
const record = value as Record<string, unknown>
|
|
359
414
|
if (record.type === 'done') {
|
|
360
415
|
terminal = true
|
|
416
|
+
const count = (value: unknown) =>
|
|
417
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0
|
|
418
|
+
outcome.summary = {
|
|
419
|
+
visited: count(record.visited),
|
|
420
|
+
ignoredDirectories: count(record.ignoredDirs),
|
|
421
|
+
...(record.stopped === 'entry-limit' ? { stopped: 'entry-limit' as const } : {}),
|
|
422
|
+
}
|
|
361
423
|
return
|
|
362
424
|
}
|
|
363
425
|
if (record.type === 'error' && typeof record.message === 'string') {
|
|
@@ -462,6 +524,7 @@ export async function* walkFilesViaExec(
|
|
|
462
524
|
}
|
|
463
525
|
} finally {
|
|
464
526
|
dispose?.()
|
|
527
|
+
if (outcome.summary) options.onSummary?.(outcome.summary)
|
|
465
528
|
if (!settled) controller.abort(new Error('File walk consumer stopped'))
|
|
466
529
|
await pending
|
|
467
530
|
// A rejected cancellation can mean remote work is still alive. Preserve
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { IGNORE_SOURCE } from './ignore-source.js'
|
|
2
|
+
|
|
3
|
+
/** One compiled ignore rule. */
|
|
4
|
+
export interface IgnoreRule {
|
|
5
|
+
readonly negate: boolean
|
|
6
|
+
readonly dirOnly: boolean
|
|
7
|
+
readonly re: RegExp
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Layered ignore rules for one depth-first walk; see `ignore-source.ts`. */
|
|
11
|
+
export interface IgnoreStack {
|
|
12
|
+
/** Find the enclosing repository and load the layers above the walk root. */
|
|
13
|
+
init(root: string): Promise<void>
|
|
14
|
+
/** Load a directory's rule files; call before reading it. */
|
|
15
|
+
enter(directory: string): Promise<void>
|
|
16
|
+
/** Drop the layer of the directory most recently entered. */
|
|
17
|
+
leave(): void
|
|
18
|
+
isIgnored(absolute: string, isDirectory: boolean): boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IgnoreDeps {
|
|
22
|
+
readonly fs: unknown
|
|
23
|
+
readonly path: unknown
|
|
24
|
+
/** Global exclude files, lowest precedence. The guest has none. */
|
|
25
|
+
readonly extraFiles?: readonly string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface IgnoreModule {
|
|
29
|
+
parse(text: string): IgnoreRule[]
|
|
30
|
+
decide(rules: readonly IgnoreRule[], relative: string, isDirectory: boolean): 0 | 1 | 2
|
|
31
|
+
create(deps: IgnoreDeps): IgnoreStack
|
|
32
|
+
vcs: readonly string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The same text is embedded in the guest program, so both walks share it.
|
|
36
|
+
const module_ = new Function(
|
|
37
|
+
`${IGNORE_SOURCE}\nreturn { parse: ignParse, decide: ignDecide, create: ignCreate, vcs: IGN_VCS }`,
|
|
38
|
+
)() as IgnoreModule
|
|
39
|
+
|
|
40
|
+
/** Parse the text of one ignore file into rules; a line it cannot read is dropped. */
|
|
41
|
+
export const parseIgnoreRules = module_.parse
|
|
42
|
+
/** 0: no rule matches, 1: ignored, 2: re-included. The last matching rule wins. */
|
|
43
|
+
export const decideIgnore = module_.decide
|
|
44
|
+
export const createIgnoreStack = module_.create
|
|
45
|
+
/** Version-control directories that no walk ever descends into. */
|
|
46
|
+
export const VCS_NAMES: ReadonlySet<string> = new Set(module_.vcs)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ignore-file matcher, as plain JavaScript source text.
|
|
3
|
+
*
|
|
4
|
+
* One implementation serves two runtimes. The host walker evaluates this text
|
|
5
|
+
* once (see `ignore-matcher.ts`); the guest program (`file-walk-program.ts`)
|
|
6
|
+
* embeds the same text, because that program travels as a single `node -e`
|
|
7
|
+
* argument with stdin closed and cannot import a package. Keeping it as
|
|
8
|
+
* source text, not a serialised function, means a host bundler cannot rewrite
|
|
9
|
+
* it, and the two walks cannot drift apart.
|
|
10
|
+
*
|
|
11
|
+
* Rules follow the gitignore format: `#` comments, `\#` and `\!` escapes,
|
|
12
|
+
* unescaped trailing spaces dropped, `!` negation, a leading or inner `/`
|
|
13
|
+
* anchors a rule to the file that holds it, a trailing `/` matches
|
|
14
|
+
* directories only, and `*`, `?`, `[...]`, `**` as git defines them. A deeper
|
|
15
|
+
* file beats a shallower one, `.ignore` beats `.gitignore` in the same
|
|
16
|
+
* directory, and the last matching line of a file wins. A rule this matcher
|
|
17
|
+
* does not understand is dropped (an unclosed `[`, or more than three
|
|
18
|
+
* separate `*` in one rule, which could backtrack for seconds), so a visible
|
|
19
|
+
* file is never hidden by one.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here is allowed to contain a backtick or a `${`: it lives inside a
|
|
22
|
+
* template literal. Every helper is prefixed `ign` so that, inlined into the
|
|
23
|
+
* guest program, it cannot collide with that program's own names.
|
|
24
|
+
*/
|
|
25
|
+
export const IGNORE_SOURCE = String.raw`
|
|
26
|
+
const IGN_MAX_BYTES = 262144;
|
|
27
|
+
const IGN_MAX_RULES = 10000;
|
|
28
|
+
const IGN_MAX_PATTERN = 1024;
|
|
29
|
+
const IGN_MAX_STARS = 3;
|
|
30
|
+
const IGN_VCS = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl'];
|
|
31
|
+
const IGN_POSIX_CLASSES = {
|
|
32
|
+
alpha: 'a-zA-Z', digit: '0-9', alnum: 'a-zA-Z0-9', upper: 'A-Z', lower: 'a-z',
|
|
33
|
+
space: ' \\t\\n\\r\\f\\v', punct: '!-\\/:-@\\[-\`{-~', xdigit: '0-9A-Fa-f',
|
|
34
|
+
};
|
|
35
|
+
function ignEscape(c) { return /[\\^$.*+?()[\]{}|\/-]/.test(c) ? '\\' + c : c; }
|
|
36
|
+
function ignClass(body) {
|
|
37
|
+
let negate = false;
|
|
38
|
+
if (body[0] === '!' || body[0] === '^') { negate = true; body = body.slice(1); }
|
|
39
|
+
let out = '';
|
|
40
|
+
for (let i = 0; i < body.length; i++) {
|
|
41
|
+
const c = body[i];
|
|
42
|
+
if (c === '[' && body[i + 1] === ':') {
|
|
43
|
+
const end = body.indexOf(':]', i + 2);
|
|
44
|
+
const name = end < 0 ? '' : body.slice(i + 2, end);
|
|
45
|
+
if (IGN_POSIX_CLASSES[name]) { out += IGN_POSIX_CLASSES[name]; i = end + 1; continue; }
|
|
46
|
+
}
|
|
47
|
+
if (c === '\\') { i++; if (i < body.length) out += ignEscape(body[i]); continue; }
|
|
48
|
+
if (c === '-') { out += (out === '' || i === body.length - 1) ? '\\-' : '-'; continue; }
|
|
49
|
+
out += (c === '[' || c === ']' || c === '^') ? '\\' + c : c;
|
|
50
|
+
}
|
|
51
|
+
if (out === '') return null;
|
|
52
|
+
return '[' + (negate ? '^/' : '') + out + ']';
|
|
53
|
+
}
|
|
54
|
+
function ignTranslate(p) {
|
|
55
|
+
let out = '';
|
|
56
|
+
let i = 0;
|
|
57
|
+
let stars = 0;
|
|
58
|
+
while (i < p.length) {
|
|
59
|
+
const c = p[i];
|
|
60
|
+
if (c === '\\') {
|
|
61
|
+
if (i + 1 >= p.length) return null;
|
|
62
|
+
out += ignEscape(p[i + 1]); i += 2; continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === '*') {
|
|
65
|
+
let j = i;
|
|
66
|
+
while (p[j] === '*') j++;
|
|
67
|
+
if (j - i >= 2 && (i === 0 || p[i - 1] === '/') && (j === p.length || p[j] === '/')) {
|
|
68
|
+
if (j === p.length) out += '.*';
|
|
69
|
+
else { out += '(?:.*/)?'; j++; }
|
|
70
|
+
i = j; continue;
|
|
71
|
+
}
|
|
72
|
+
stars++;
|
|
73
|
+
out += '[^/]*'; i = j; continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === '?') { out += '[^/]'; i++; continue; }
|
|
76
|
+
if (c === '[') {
|
|
77
|
+
let j = i + 1;
|
|
78
|
+
if (p[j] === '!' || p[j] === '^') j++;
|
|
79
|
+
if (p[j] === ']') j++;
|
|
80
|
+
while (j < p.length && p[j] !== ']') { if (p[j] === '\\') j++; j++; }
|
|
81
|
+
if (j >= p.length) return null;
|
|
82
|
+
const cls = ignClass(p.slice(i + 1, j));
|
|
83
|
+
if (cls === null) return null;
|
|
84
|
+
out += cls; i = j + 1; continue;
|
|
85
|
+
}
|
|
86
|
+
out += ignEscape(c); i++;
|
|
87
|
+
}
|
|
88
|
+
// Each separate star can backtrack against the others: a rule with many of
|
|
89
|
+
// them takes time exponential in their number on one long name. Real rules
|
|
90
|
+
// use one or two; a rule past the cap is dropped, like any rule not understood.
|
|
91
|
+
if (stars > IGN_MAX_STARS) return null;
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** One line to one rule, or null when the line holds none. */
|
|
95
|
+
function ignRule(raw) {
|
|
96
|
+
let line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
97
|
+
while (line.endsWith(' ') && !line.endsWith('\\ ')) line = line.slice(0, -1);
|
|
98
|
+
if (line === '' || line[0] === '#' || line.length > IGN_MAX_PATTERN) return null;
|
|
99
|
+
let negate = false;
|
|
100
|
+
if (line[0] === '!') { negate = true; line = line.slice(1); }
|
|
101
|
+
let dirOnly = false;
|
|
102
|
+
while (line.length > 1 && line.endsWith('/')) { dirOnly = true; line = line.slice(0, -1); }
|
|
103
|
+
if (line === '/' || line === '') return null;
|
|
104
|
+
const anchored = line.indexOf('/') >= 0;
|
|
105
|
+
if (line[0] === '/') line = line.slice(1);
|
|
106
|
+
line = line.replace(/(\*\*\/)+/g, '**/');
|
|
107
|
+
if ((line.match(/\*\*/g) || []).length > 4) return null;
|
|
108
|
+
let body;
|
|
109
|
+
try { body = ignTranslate(line); } catch (error) { return null; }
|
|
110
|
+
if (body === null) return null;
|
|
111
|
+
try {
|
|
112
|
+
return { negate, dirOnly, re: new RegExp((anchored ? '^' : '^(?:.*/)?') + body + '$') };
|
|
113
|
+
} catch (error) { return null; }
|
|
114
|
+
}
|
|
115
|
+
function ignParse(text) {
|
|
116
|
+
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
|
|
117
|
+
if (text.length > IGN_MAX_BYTES) text = text.slice(0, text.lastIndexOf('\n', IGN_MAX_BYTES) + 1);
|
|
118
|
+
const rules = [];
|
|
119
|
+
for (const line of text.split('\n')) {
|
|
120
|
+
const rule = ignRule(line);
|
|
121
|
+
if (rule) rules.push(rule);
|
|
122
|
+
if (rules.length >= IGN_MAX_RULES) break;
|
|
123
|
+
}
|
|
124
|
+
return rules;
|
|
125
|
+
}
|
|
126
|
+
/** 0: no rule matches. 1: ignored. 2: re-included. The last matching rule wins. */
|
|
127
|
+
function ignDecide(rules, relative, isDirectory) {
|
|
128
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
129
|
+
const rule = rules[i];
|
|
130
|
+
if (rule.dirOnly && !isDirectory) continue;
|
|
131
|
+
if (rule.re.test(relative)) return rule.negate ? 2 : 1;
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The layered rules for one walk. The caller enters a directory before it
|
|
137
|
+
* reads it and leaves it after, so the layers follow the depth-first stack.
|
|
138
|
+
* deps: fs (promises), path, extraFiles (global exclude files, host only).
|
|
139
|
+
*/
|
|
140
|
+
function ignCreate(deps) {
|
|
141
|
+
const fs = deps.fs;
|
|
142
|
+
const path = deps.path;
|
|
143
|
+
const layers = [];
|
|
144
|
+
const frames = [];
|
|
145
|
+
let ancestorsInRepo = false;
|
|
146
|
+
const posix = value => path.sep === '/' ? value : value.split(path.sep).join('/');
|
|
147
|
+
const lstat = async file => { try { return await fs.lstat(file); } catch (error) { return null; } };
|
|
148
|
+
const load = async file => {
|
|
149
|
+
const info = await lstat(file);
|
|
150
|
+
if (!info || !info.isFile() || info.size > IGN_MAX_BYTES * 4) return [];
|
|
151
|
+
try { return ignParse(await fs.readFile(file, 'utf8')); } catch (error) { return []; }
|
|
152
|
+
};
|
|
153
|
+
const loadDirectory = async (directory, inRepo) => {
|
|
154
|
+
const rules = [];
|
|
155
|
+
if (inRepo) rules.push(...await load(path.join(directory, '.gitignore')));
|
|
156
|
+
rules.push(...await load(path.join(directory, '.ignore')));
|
|
157
|
+
return rules;
|
|
158
|
+
};
|
|
159
|
+
const push = (directory, rules) => {
|
|
160
|
+
layers.push({ base: posix(directory).replace(/\/$/, ''), rules });
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
/** Find the enclosing repository and load the layers above the root. */
|
|
164
|
+
async init(root) {
|
|
165
|
+
let directory = root;
|
|
166
|
+
let repo = null;
|
|
167
|
+
for (;;) {
|
|
168
|
+
const info = await lstat(path.join(directory, '.git'));
|
|
169
|
+
if (info) { repo = { directory, isDirectory: info.isDirectory() }; break; }
|
|
170
|
+
const parent = path.dirname(directory);
|
|
171
|
+
if (parent === directory) break;
|
|
172
|
+
directory = parent;
|
|
173
|
+
}
|
|
174
|
+
if (!repo || repo.directory === root) return;
|
|
175
|
+
ancestorsInRepo = true;
|
|
176
|
+
const chain = [];
|
|
177
|
+
for (let up = path.dirname(root); ; up = path.dirname(up)) {
|
|
178
|
+
chain.unshift(up);
|
|
179
|
+
if (up === repo.directory || path.dirname(up) === up) break;
|
|
180
|
+
}
|
|
181
|
+
for (const [index, directory] of chain.entries()) {
|
|
182
|
+
const rules = [];
|
|
183
|
+
if (index === 0) {
|
|
184
|
+
for (const file of deps.extraFiles || []) rules.push(...await load(file));
|
|
185
|
+
if (repo.isDirectory) rules.push(...await load(path.join(directory, '.git', 'info', 'exclude')));
|
|
186
|
+
}
|
|
187
|
+
rules.push(...await loadDirectory(directory, true));
|
|
188
|
+
push(directory, rules);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
async enter(directory) {
|
|
192
|
+
const parentInRepo = frames.length ? frames[frames.length - 1] : ancestorsInRepo;
|
|
193
|
+
let inRepo = parentInRepo;
|
|
194
|
+
const rules = [];
|
|
195
|
+
if (!parentInRepo) {
|
|
196
|
+
const info = await lstat(path.join(directory, '.git'));
|
|
197
|
+
if (info) {
|
|
198
|
+
inRepo = true;
|
|
199
|
+
for (const file of deps.extraFiles || []) rules.push(...await load(file));
|
|
200
|
+
if (info.isDirectory()) rules.push(...await load(path.join(directory, '.git', 'info', 'exclude')));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
rules.push(...await loadDirectory(directory, inRepo));
|
|
204
|
+
frames.push(inRepo);
|
|
205
|
+
push(directory, rules);
|
|
206
|
+
},
|
|
207
|
+
leave() { frames.pop(); layers.pop(); },
|
|
208
|
+
isIgnored(absolute, isDirectory) {
|
|
209
|
+
const target = posix(absolute);
|
|
210
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
211
|
+
const layer = layers[i];
|
|
212
|
+
if (layer.rules.length === 0 || !target.startsWith(layer.base + '/')) continue;
|
|
213
|
+
const decision = ignDecide(layer.rules, target.slice(layer.base.length + 1), isDirectory);
|
|
214
|
+
if (decision) return decision === 1;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
`
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from 'node:os'
|
|
|
14
14
|
import { dirname, isAbsolute, join, parse, resolve } from 'node:path'
|
|
15
15
|
import { NAMZU } from '../../constants/telemetry/index.js'
|
|
16
16
|
import { SCOPE_ATTRIBUTE } from '../../utils/log/types.js'
|
|
17
|
+
import { attributeSandboxFailure } from '../denial.js'
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
SANDBOX_DEFAULT_TIMEOUT_MS,
|
|
@@ -1084,8 +1085,16 @@ class LocalSandbox implements Sandbox {
|
|
|
1084
1085
|
terminate('SIGKILL')
|
|
1085
1086
|
}
|
|
1086
1087
|
const timedOut = cancellation === 'deadline'
|
|
1088
|
+
const exitCode = code ?? (timedOut ? 124 : 1)
|
|
1089
|
+
const attribution = attributeSandboxFailure({
|
|
1090
|
+
environment: this.environment,
|
|
1091
|
+
exitCode,
|
|
1092
|
+
stderr: stderr.text,
|
|
1093
|
+
timedOut,
|
|
1094
|
+
allowedRoots: [...this.roots, tmpdir(), '/tmp', '/var/tmp', '/dev'],
|
|
1095
|
+
})
|
|
1087
1096
|
resolvePromise({
|
|
1088
|
-
exitCode
|
|
1097
|
+
exitCode,
|
|
1089
1098
|
stdout: stdout.text,
|
|
1090
1099
|
stderr: stderr.text,
|
|
1091
1100
|
signal: signal ?? undefined,
|
|
@@ -1096,6 +1105,7 @@ class LocalSandbox implements Sandbox {
|
|
|
1096
1105
|
// gone. The tool layer already renders the flag.
|
|
1097
1106
|
stdoutTruncated: stdout.truncated,
|
|
1098
1107
|
stderrTruncated: stderr.truncated,
|
|
1108
|
+
...(attribution !== undefined ? { attribution } : {}),
|
|
1099
1109
|
})
|
|
1100
1110
|
})
|
|
1101
1111
|
|
|
@@ -789,6 +789,7 @@ export function formatCompletionNotification(handles: readonly TaskHandle[]): st
|
|
|
789
789
|
: []),
|
|
790
790
|
...(handle.result?.stopReason ? [`stop_reason: ${handle.result.stopReason}`] : []),
|
|
791
791
|
...(durationMs !== undefined ? [`duration_ms: ${durationMs}`] : []),
|
|
792
|
+
...(handle.completedAt ? [`finished_at: ${new Date(handle.completedAt).toISOString()}`] : []),
|
|
792
793
|
'',
|
|
793
794
|
body,
|
|
794
795
|
// Outside the envelope, deliberately: this sentence is an
|
package/src/session/log-hash.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import {
|
|
3
|
+
IgnorableRecordSchema,
|
|
3
4
|
SESSION_RECORD_MAX_BYTES,
|
|
4
5
|
type SessionRecord,
|
|
5
6
|
SessionRecordSchema,
|
|
@@ -99,6 +100,18 @@ export function parseSessionLogLine(line: string | Uint8Array): ParsedSessionLog
|
|
|
99
100
|
)
|
|
100
101
|
}
|
|
101
102
|
const parsed = SessionRecordSchema.safeParse(value)
|
|
103
|
+
if (!parsed.success && isFlaggedIgnorable(value)) {
|
|
104
|
+
const ignorable = IgnorableRecordSchema.safeParse(value)
|
|
105
|
+
if (ignorable.success) {
|
|
106
|
+
// Typed as a record so the chain can carry it; its `type` is one no
|
|
107
|
+
// consumer matches, so it never moves a fold.
|
|
108
|
+
return {
|
|
109
|
+
record: ignorable.data as unknown as SessionRecord,
|
|
110
|
+
length: bytes.byteLength,
|
|
111
|
+
sha256: recordSha256(bytes),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
102
115
|
if (!parsed.success) {
|
|
103
116
|
throw new SessionLogLineError(
|
|
104
117
|
'not-a-record',
|
|
@@ -113,3 +126,11 @@ export function parseSessionLogLine(line: string | Uint8Array): ParsedSessionLog
|
|
|
113
126
|
sha256: recordSha256(bytes),
|
|
114
127
|
}
|
|
115
128
|
}
|
|
129
|
+
|
|
130
|
+
function isFlaggedIgnorable(value: unknown): boolean {
|
|
131
|
+
return (
|
|
132
|
+
typeof value === 'object' &&
|
|
133
|
+
value !== null &&
|
|
134
|
+
(value as { ignorable?: unknown }).ignorable === true
|
|
135
|
+
)
|
|
136
|
+
}
|