@namzu/sdk 62.7.0 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  3. package/dist/manager/session/turn-recorder.js +50 -26
  4. package/dist/manager/session/turn-recorder.js.map +1 -1
  5. package/dist/runtime/query/context.d.ts.map +1 -1
  6. package/dist/runtime/query/context.js +28 -2
  7. package/dist/runtime/query/context.js.map +1 -1
  8. package/dist/runtime/query/executor.d.ts.map +1 -1
  9. package/dist/runtime/query/executor.js +24 -2
  10. package/dist/runtime/query/executor.js.map +1 -1
  11. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  12. package/dist/runtime/query/prepare-turn.js +17 -8
  13. package/dist/runtime/query/prepare-turn.js.map +1 -1
  14. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  15. package/dist/runtime/query/session-storage.js +44 -3
  16. package/dist/runtime/query/session-storage.js.map +1 -1
  17. package/dist/sandbox/file-walk.d.ts +7 -1
  18. package/dist/sandbox/file-walk.d.ts.map +1 -1
  19. package/dist/sandbox/file-walk.js +30 -4
  20. package/dist/sandbox/file-walk.js.map +1 -1
  21. package/dist/store/checkpoint/contract.d.ts +12 -0
  22. package/dist/store/checkpoint/contract.d.ts.map +1 -1
  23. package/dist/store/checkpoint/contract.js.map +1 -1
  24. package/dist/store/checkpoint/disk.d.ts.map +1 -1
  25. package/dist/store/checkpoint/disk.js +34 -2
  26. package/dist/store/checkpoint/disk.js.map +1 -1
  27. package/dist/store/checkpoint/prune.d.ts.map +1 -1
  28. package/dist/store/checkpoint/prune.js +6 -0
  29. package/dist/store/checkpoint/prune.js.map +1 -1
  30. package/dist/store/evidence/disk.js +40 -3
  31. package/dist/store/evidence/disk.js.map +1 -1
  32. package/dist/store/session-log/incremental.d.ts +23 -0
  33. package/dist/store/session-log/incremental.d.ts.map +1 -0
  34. package/dist/store/session-log/incremental.js +117 -0
  35. package/dist/store/session-log/incremental.js.map +1 -0
  36. package/dist/tools/admit-facts.d.ts.map +1 -1
  37. package/dist/tools/admit-facts.js +1 -0
  38. package/dist/tools/admit-facts.js.map +1 -1
  39. package/dist/tools/builtins/bash.d.ts.map +1 -1
  40. package/dist/tools/builtins/bash.js +10 -1
  41. package/dist/tools/builtins/bash.js.map +1 -1
  42. package/dist/tools/builtins/bounded-file-read.d.ts +19 -1
  43. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -1
  44. package/dist/tools/builtins/bounded-file-read.js +56 -3
  45. package/dist/tools/builtins/bounded-file-read.js.map +1 -1
  46. package/dist/tools/builtins/grep.d.ts +8 -0
  47. package/dist/tools/builtins/grep.d.ts.map +1 -1
  48. package/dist/tools/builtins/grep.js +147 -20
  49. package/dist/tools/builtins/grep.js.map +1 -1
  50. package/dist/tools/builtins/search-support.d.ts +8 -0
  51. package/dist/tools/builtins/search-support.d.ts.map +1 -1
  52. package/dist/tools/builtins/search-support.js +66 -0
  53. package/dist/tools/builtins/search-support.js.map +1 -1
  54. package/dist/types/tool/presentation.d.ts +6 -0
  55. package/dist/types/tool/presentation.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/manager/session/turn-recorder.ts +53 -27
  58. package/src/runtime/query/context.ts +33 -6
  59. package/src/runtime/query/executor.ts +24 -2
  60. package/src/runtime/query/prepare-turn.ts +16 -8
  61. package/src/runtime/query/session-storage.ts +42 -3
  62. package/src/sandbox/file-walk.ts +41 -4
  63. package/src/store/checkpoint/contract.ts +15 -0
  64. package/src/store/checkpoint/disk.ts +34 -2
  65. package/src/store/checkpoint/prune.ts +13 -0
  66. package/src/store/evidence/disk.ts +57 -2
  67. package/src/store/session-log/incremental.ts +147 -0
  68. package/src/tools/admit-facts.ts +1 -0
  69. package/src/tools/builtins/bash.ts +11 -1
  70. package/src/tools/builtins/bounded-file-read.ts +59 -2
  71. package/src/tools/builtins/grep.ts +172 -26
  72. package/src/tools/builtins/search-support.ts +62 -0
  73. package/src/types/tool/presentation.ts +6 -0
@@ -1 +1 @@
1
- {"version":3,"file":"presentation.d.ts","sourceRoot":"","sources":["../../../src/types/tool/presentation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"presentation.d.ts","sourceRoot":"","sources":["../../../src/types/tool/presentation.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "62.7.0",
3
+ "version": "62.8.0",
4
4
  "description": "Open-source AI kernel for TypeScript: runs an agent the way an operating system runs a process, with an identity and a budget, scheduled tool calls and checkpointed turns.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "engines": {
@@ -7,6 +7,7 @@ import { NAMZU } from '../../constants/telemetry/index.js'
7
7
  import { resolveModelPricing } from '../../pricing/index.js'
8
8
  import type { SessionTokenBudget } from '../../store/budget/index.js'
9
9
  import type { SessionCheckpointStore } from '../../store/checkpoint/index.js'
10
+ import { foldSessionLog } from '../../store/session-log/incremental.js'
10
11
  import {
11
12
  type SessionLease,
12
13
  type SessionLog,
@@ -1441,12 +1442,29 @@ export async function readFoldedHistory(
1441
1442
  log: SessionLog,
1442
1443
  options: { readonly throughSeq?: number } = {},
1443
1444
  ): Promise<RecordedMessage[]> {
1445
+ // Folding the whole log is advanced from the last record the previous call
1446
+ // folded, not redone from byte 0 on every turn. A fold to a seq is a
1447
+ // different question and is read in full.
1448
+ if (options.throughSeq === undefined) {
1449
+ return foldSessionLog(log, {
1450
+ name: 'folded-history',
1451
+ mode: 'strict',
1452
+ create: () => new SessionMessageFold(),
1453
+ apply: (fold, entry) => fold.apply(entry.record as SessionRecord),
1454
+ result: (fold) => recordedMessages(log, fold),
1455
+ })
1456
+ }
1444
1457
  const fold = new SessionMessageFold()
1445
- for await (const entry of log.read(
1446
- options.throughSeq !== undefined ? { throughSeq: options.throughSeq } : {},
1447
- )) {
1458
+ for await (const entry of log.read({ throughSeq: options.throughSeq })) {
1448
1459
  fold.apply(entry.record as SessionRecord)
1449
1460
  }
1461
+ return recordedMessages(log, fold)
1462
+ }
1463
+
1464
+ async function recordedMessages(
1465
+ log: SessionLog,
1466
+ fold: SessionMessageFold,
1467
+ ): Promise<RecordedMessage[]> {
1450
1468
  const out: RecordedMessage[] = []
1451
1469
  const spilled = fold.spilledSummary
1452
1470
  if (spilled) {
@@ -1463,9 +1481,10 @@ export async function readFoldedHistory(
1463
1481
  }
1464
1482
  }
1465
1483
  for (const entry of fold.entries()) {
1484
+ // A message from a shared fold is copied: the caller owns what it gets.
1466
1485
  const message = entry.spill
1467
1486
  ? (JSON.parse(await log.readSpill(entry.spill as SpillRef)) as Message)
1468
- : entry.message
1487
+ : structuredClone(entry.message)
1469
1488
  out.push({
1470
1489
  message: withMessageId(message, entry.messageId),
1471
1490
  ...(entry.messageId ? { messageId: entry.messageId } : {}),
@@ -1488,29 +1507,36 @@ export async function readFoldedHistory(
1488
1507
  export async function readEverRecordedMessages(
1489
1508
  log: SessionLog,
1490
1509
  ): Promise<ReadonlyMap<MessageId, Message>> {
1491
- const latest = new Map<MessageId, { readonly content: Message; readonly spill?: SpillRef }>()
1492
- for await (const entry of log.read()) {
1493
- const record = entry.record as SessionRecord
1494
- if (record.type === 'message') {
1495
- latest.set(record.messageId, {
1496
- content: record.content,
1497
- ...(record.spill ? { spill: record.spill } : {}),
1498
- })
1499
- } else if (record.type === 'message_replaced') {
1500
- latest.set(record.targetMessageId, {
1501
- content: record.content,
1502
- ...(record.spill ? { spill: record.spill } : {}),
1503
- })
1504
- }
1505
- }
1506
- const resolved = new Map<MessageId, Message>()
1507
- for (const [id, entry] of latest) {
1508
- const content = entry.spill
1509
- ? (JSON.parse(await log.readSpill(entry.spill)) as Message)
1510
- : entry.content
1511
- resolved.set(id, withMessageId(content, id))
1512
- }
1513
- return resolved
1510
+ return foldSessionLog(log, {
1511
+ name: 'ever-recorded-messages',
1512
+ mode: 'strict',
1513
+ create: () => new Map<MessageId, { readonly content: Message; readonly spill?: SpillRef }>(),
1514
+ apply: (latest, entry) => {
1515
+ const record = entry.record as SessionRecord
1516
+ if (record.type === 'message') {
1517
+ latest.set(record.messageId, {
1518
+ content: record.content,
1519
+ ...(record.spill ? { spill: record.spill } : {}),
1520
+ })
1521
+ } else if (record.type === 'message_replaced') {
1522
+ latest.set(record.targetMessageId, {
1523
+ content: record.content,
1524
+ ...(record.spill ? { spill: record.spill } : {}),
1525
+ })
1526
+ }
1527
+ },
1528
+ result: async (latest) => {
1529
+ const resolved = new Map<MessageId, Message>()
1530
+ for (const [id, entry] of latest) {
1531
+ // Copied: the map this is read from is shared with the next call.
1532
+ const content = entry.spill
1533
+ ? (JSON.parse(await log.readSpill(entry.spill)) as Message)
1534
+ : structuredClone(entry.content)
1535
+ resolved.set(id, withMessageId(content, id))
1536
+ }
1537
+ return resolved
1538
+ },
1539
+ })
1514
1540
  }
1515
1541
 
1516
1542
  /**
@@ -92,6 +92,34 @@ export interface TurnContext {
92
92
  trackingConfig: ActivityTrackingConfig
93
93
  }
94
94
 
95
+ /**
96
+ * Forward `signal`'s abort to `controller` without keeping the controller, or
97
+ * anything the turn built around it, alive for the life of `signal`.
98
+ *
99
+ * The session's signal outlives every turn, and a listener on it is strongly
100
+ * held for as long as the signal is. A listener that closed over the turn's
101
+ * controller (and the configuration it was built from) kept each finished
102
+ * turn's whole object graph in memory, one more per turn. This one holds the
103
+ * controller through a `WeakRef`, lives in its own scope so the closure
104
+ * captures nothing else, and is removed once the controller is collected.
105
+ */
106
+ function relayAbort(signal: AbortSignal, controller: AbortController): void {
107
+ const controllerRef = new WeakRef(controller)
108
+ const signalRef = new WeakRef(signal)
109
+ const relay = (): void => {
110
+ controllerRef.deref()?.abort(signalRef.deref()?.reason)
111
+ }
112
+ signal.addEventListener('abort', relay, { once: true })
113
+ relayCleanup.register(controller, { signalRef, relay })
114
+ }
115
+
116
+ const relayCleanup = new FinalizationRegistry<{
117
+ signalRef: WeakRef<AbortSignal>
118
+ relay: () => void
119
+ }>(({ signalRef, relay }) => {
120
+ signalRef.deref()?.removeEventListener('abort', relay)
121
+ })
122
+
95
123
  export class TurnContextFactory {
96
124
  /**
97
125
  * The turn's one correlated logger, built once and handed to every
@@ -136,11 +164,7 @@ export class TurnContextFactory {
136
164
  if (config.signal.aborted) {
137
165
  abortController.abort(config.signal.reason)
138
166
  } else {
139
- config.signal.addEventListener(
140
- 'abort',
141
- () => abortController.abort(config.signal?.reason),
142
- { once: true },
143
- )
167
+ relayAbort(config.signal, abortController)
144
168
  }
145
169
  }
146
170
 
@@ -178,7 +202,10 @@ export class TurnContextFactory {
178
202
  config.enableActivityTracking,
179
203
  )
180
204
  const activityStore = new ActivityStore(config.turnId, trackingConfig)
181
- const planManager = new PlanManager({ sessionId: config.sessionId, turnId: config.turnId })
205
+ const planManager = new PlanManager({
206
+ sessionId: config.sessionId,
207
+ turnId: config.turnId,
208
+ })
182
209
  const toolResultsDir =
183
210
  config.storage.sessionDir === undefined
184
211
  ? undefined
@@ -705,7 +705,7 @@ export type PriorToolResults = ReadonlyMap<string, PriorToolResult>
705
705
  * for administrator rights is named as that; everything else is a plain block.
706
706
  * Read from the call's own argument, never from the refusal's wording.
707
707
  */
708
- function refusalFacts(toolName: string, input: unknown): ToolFacts {
708
+ function refusalFacts(toolName: string, input: unknown, reason?: string): ToolFacts {
709
709
  const command =
710
710
  input && typeof input === 'object' ? (input as { command?: unknown }).command : undefined
711
711
  const elevated = typeof command === 'string' && /(^|[\s;&|(])(sudo|doas)\b/.test(command)
@@ -713,9 +713,31 @@ function refusalFacts(toolName: string, input: unknown): ToolFacts {
713
713
  kind: 'failure',
714
714
  reason: elevated ? 'needs_administrator' : 'blocked',
715
715
  subject: toolName,
716
+ ...(refusalNote(reason) ? { note: refusalNote(reason) } : {}),
716
717
  }
717
718
  }
718
719
 
720
+ const REFUSAL_NOTE_MAX = 200
721
+
722
+ /**
723
+ * The refusal's own reason as one line for the person's view: the first
724
+ * non-empty line, the leading "Blocked:" label dropped, first letter up, bounded. The policy
725
+ * wrote it; nothing here reads the model's text.
726
+ */
727
+ function refusalNote(reason: string | undefined): string | undefined {
728
+ const line = reason
729
+ ?.split(/\r?\n/)
730
+ .map((part) => part.trim())
731
+ .find(Boolean)
732
+ if (!line) return undefined
733
+ const plain = line.replace(/^Blocked(?: by [^:]{1,60})?:\s*/i, '').trim()
734
+ if (!plain) return undefined
735
+ const sentence = `${plain.charAt(0).toUpperCase()}${plain.slice(1)}`
736
+ return sentence.length > REFUSAL_NOTE_MAX
737
+ ? `${sentence.slice(0, REFUSAL_NOTE_MAX - 1)}…`
738
+ : sentence
739
+ }
740
+
719
741
  export function deniedToolOutput(toolName: string, reason: string): string {
720
742
  return `Error: Tool "${toolName}" was not executed. ${reason}`
721
743
  }
@@ -3633,7 +3655,7 @@ export class ToolExecutor {
3633
3655
  ...(declined ? { presentation: this.declinedPresentation(toolName, input, declined) } : {}),
3634
3656
  // A refusal by policy says what kind it was; the person's own No is
3635
3657
  // already carried by the presentation above.
3636
- ...(declined ? {} : { facts: refusalFacts(toolName, input) }),
3658
+ ...(declined ? {} : { facts: refusalFacts(toolName, input, reason) }),
3637
3659
  isError: true,
3638
3660
  })
3639
3661
 
@@ -23,6 +23,7 @@ import { withProviderRetry } from '../../provider/retry.js'
23
23
  import { withTokenBudget } from '../../provider/token-budget.js'
24
24
  import { resolveAttachments } from '../../store/attachment/index.js'
25
25
  import type { SessionTokenBudget } from '../../store/budget/index.js'
26
+ import { foldSessionLog } from '../../store/session-log/incremental.js'
26
27
  import type { SessionLog } from '../../store/session-log/index.js'
27
28
  import { NAMZU } from '../../telemetry/attributes.js'
28
29
  import type { SerializedSpanContext } from '../../telemetry/attributes.js'
@@ -1232,14 +1233,21 @@ async function assertTurnIdIsNew(
1232
1233
  storage: SessionStorage,
1233
1234
  ): Promise<void> {
1234
1235
  if (params.turnId === undefined) return
1235
- for await (const { record } of storage.log.read({ mode: 'tolerant' })) {
1236
- if (record.type === 'turn_started' && record.turnId === turnId) {
1237
- throw new NamzuError({
1238
- code: 'invalid_config',
1239
- message: `Turn ${turnId} already exists in session ${params.sessionId}. A new turn needs a new id; to continue that turn, pass resumeFromCheckpoint.`,
1240
- details: { sessionId: params.sessionId, turnId },
1241
- })
1242
- }
1236
+ const exists = await foldSessionLog(storage.log, {
1237
+ name: 'turn-ids',
1238
+ mode: 'tolerant',
1239
+ create: () => new Set<string>(),
1240
+ apply: (ids, { record }) => {
1241
+ if (record.type === 'turn_started') ids.add(record.turnId)
1242
+ },
1243
+ result: (ids) => ids.has(turnId),
1244
+ })
1245
+ if (exists) {
1246
+ throw new NamzuError({
1247
+ code: 'invalid_config',
1248
+ message: `Turn ${turnId} already exists in session ${params.sessionId}. A new turn needs a new id; to continue that turn, pass resumeFromCheckpoint.`,
1249
+ details: { sessionId: params.sessionId, turnId },
1250
+ })
1243
1251
  }
1244
1252
  }
1245
1253
 
@@ -14,6 +14,7 @@ import {
14
14
  InMemorySessionCheckpointStore,
15
15
  type SessionCheckpointStore,
16
16
  } from '../../store/checkpoint/index.js'
17
+ import { foldSessionLog } from '../../store/session-log/incremental.js'
17
18
  import {
18
19
  DiskSessionLog,
19
20
  type InMemoryLogMedium,
@@ -175,7 +176,10 @@ export async function locateSession(
175
176
  const parent = (await findSessionLocator(paths, parentSessionId)) ?? {
176
177
  sessionId: parentSessionId,
177
178
  }
178
- return { sessionId, ancestors: [...(parent.ancestors ?? []), parent.sessionId] }
179
+ return {
180
+ sessionId,
181
+ ancestors: [...(parent.ancestors ?? []), parent.sessionId],
182
+ }
179
183
  }
180
184
 
181
185
  /** Deeper than any delegation tree a project allows; a guard against a cyclic tree on disk. */
@@ -234,7 +238,10 @@ async function listDirectory(
234
238
  /** `SessionPaths` for the working directory's project under `resolveNamzuHome()`. */
235
239
  export async function defaultSessionPaths(workingDirectory?: string): Promise<SessionPaths> {
236
240
  const home = resolveNamzuHome()
237
- const project = await ensureProject({ home, cwd: workingDirectory ?? process.cwd() })
241
+ const project = await ensureProject({
242
+ home,
243
+ cwd: workingDirectory ?? process.cwd(),
244
+ })
238
245
  return new SessionPaths({ home, slug: project.slug })
239
246
  }
240
247
 
@@ -242,7 +249,9 @@ function heldState(log: InMemorySessionLog): HeldState {
242
249
  const current = held.get(log.medium)
243
250
  if (current) return current
244
251
  const next: HeldState = {
245
- checkpoints: new InMemorySessionCheckpointStore({ log: checkpointLogView(log) }),
252
+ checkpoints: new InMemorySessionCheckpointStore({
253
+ log: checkpointLogView(log),
254
+ }),
246
255
  tokenBudgets: new InMemorySessionTokenBudgetStore(),
247
256
  }
248
257
  held.set(log.medium, next)
@@ -303,5 +312,35 @@ export function checkpointLogView(log: SessionLog): CheckpointLogView {
303
312
  }
304
313
  return open.values()
305
314
  },
315
+ async pruneFacts(scope, checkpointIds) {
316
+ own(scope)
317
+ // Every committed checkpoint and every open decision, advanced from the
318
+ // last record the previous prune folded rather than read from byte 0:
319
+ // a prune runs every iteration of every turn.
320
+ return foldSessionLog(log, {
321
+ name: 'prune-facts',
322
+ mode: 'tolerant',
323
+ create: () => ({
324
+ written: new Map<CheckpointId, string>(),
325
+ open: new Map<string, CheckpointId>(),
326
+ }),
327
+ apply: ({ written, open }, { record }) => {
328
+ if (record.type === 'checkpoint_written')
329
+ written.set(record.checkpointId, record.docSha256)
330
+ if (record.type === 'decision_requested') open.set(record.decisionId, record.checkpointId)
331
+ if (record.type === 'decision_resolved' || record.type === 'decision_expired') {
332
+ open.delete(record.decisionId)
333
+ }
334
+ },
335
+ result: ({ written, open }) => {
336
+ const wanted = new Map<CheckpointId, string>()
337
+ for (const id of checkpointIds) {
338
+ const sha256 = written.get(id)
339
+ if (sha256 !== undefined) wanted.set(id, sha256)
340
+ }
341
+ return { written: wanted, open: [...open.values()] }
342
+ },
343
+ })
344
+ },
306
345
  }
307
346
  }
@@ -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
- const entry = await wait(frame.dir.read())
331
- check()
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(planWalk(rootPath, options), options.signal, options.onSummary)
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. */
@@ -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 text = await readIfPresent(this.#paths.checkpointFile(locator, id))
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(handle, size, scope)
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
  }