@sayknow-cli/coding-agent 0.5.1 → 0.5.6

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 (122) hide show
  1. package/CHANGELOG.md +185 -14
  2. package/dist/types/cli/update-cli.d.ts +8 -1
  3. package/dist/types/commands/session.d.ts +7 -0
  4. package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-registry.d.ts +1 -1
  7. package/dist/types/config/models-config-schema.d.ts +4 -0
  8. package/dist/types/config/settings-schema.d.ts +9 -0
  9. package/dist/types/config/telegram-autostart.d.ts +9 -1
  10. package/dist/types/dap/client.d.ts +1 -0
  11. package/dist/types/lsp/client.d.ts +5 -0
  12. package/dist/types/modes/components/pet-capability.d.ts +8 -7
  13. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  14. package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
  15. package/dist/types/modes/components/welcome.d.ts +1 -0
  16. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +13 -1
  18. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
  19. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
  20. package/dist/types/modes/types.d.ts +7 -1
  21. package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
  22. package/dist/types/session/agent-session.d.ts +9 -0
  23. package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
  24. package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
  25. package/dist/types/session/session-manager.d.ts +9 -0
  26. package/dist/types/session/session-storage.d.ts +17 -0
  27. package/dist/types/session-import/claude.d.ts +26 -0
  28. package/dist/types/session-import/codex.d.ts +22 -0
  29. package/dist/types/session-import/command.d.ts +20 -0
  30. package/dist/types/session-import/detect.d.ts +22 -0
  31. package/dist/types/session-import/index.d.ts +7 -0
  32. package/dist/types/session-import/redact.d.ts +23 -0
  33. package/dist/types/session-import/service.d.ts +28 -0
  34. package/dist/types/session-import/types.d.ts +125 -0
  35. package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
  36. package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
  37. package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
  38. package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
  39. package/dist/types/skc-runtime/session-restore.d.ts +99 -0
  40. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
  41. package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
  42. package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
  43. package/dist/types/slash-commands/types.d.ts +5 -0
  44. package/dist/types/tools/ask.d.ts +169 -4
  45. package/package.json +16 -7
  46. package/scripts/generate-sdk-operation-inventory.ts +5 -6
  47. package/src/cli/config-cli.ts +23 -8
  48. package/src/cli/update-cli.ts +265 -21
  49. package/src/commands/session.ts +88 -2
  50. package/src/config/atomic-yaml-patch.ts +167 -30
  51. package/src/config/keybindings.ts +10 -0
  52. package/src/config/model-profiles.ts +69 -1
  53. package/src/config/model-registry.ts +72 -38
  54. package/src/config/models-config-schema.ts +1 -1
  55. package/src/config/settings-schema.ts +9 -0
  56. package/src/config/settings.ts +13 -2
  57. package/src/config/telegram-autostart.ts +11 -4
  58. package/src/dap/client.ts +78 -30
  59. package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
  60. package/src/internal-urls/docs-index.generated.ts +5 -4
  61. package/src/lsp/client.ts +17 -29
  62. package/src/main.ts +1 -1
  63. package/src/modes/action-registry.ts +2 -0
  64. package/src/modes/components/pet-capability.ts +22 -13
  65. package/src/modes/components/pet-selector.ts +1 -1
  66. package/src/modes/components/sayknow-pet-widget.ts +41 -7
  67. package/src/modes/components/welcome.ts +5 -0
  68. package/src/modes/controllers/event-controller.ts +1 -1
  69. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  70. package/src/modes/controllers/input-controller.ts +71 -6
  71. package/src/modes/controllers/plan-mode-controller.ts +8 -1
  72. package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
  73. package/src/modes/controllers/selector-controller.ts +47 -19
  74. package/src/modes/interactive-mode.ts +54 -4
  75. package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
  76. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
  77. package/src/modes/types.ts +5 -1
  78. package/src/notifications/lifecycle-control-runtime.ts +258 -179
  79. package/src/prompts/system/eager-todo.md +2 -0
  80. package/src/prompts/system/plan-mode-approved.md +1 -1
  81. package/src/prompts/system/system-prompt.md +4 -2
  82. package/src/runtime-mcp/transports/stdio.ts +96 -33
  83. package/src/sdk/broker/lifecycle.ts +6 -5
  84. package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
  85. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  86. package/src/sdk/session.ts +25 -18
  87. package/src/session/agent-session.ts +120 -25
  88. package/src/session/internal/managed-session-scope.ts +41 -6
  89. package/src/session/internal/managed-session-storage.ts +14 -0
  90. package/src/session/session-manager.ts +113 -11
  91. package/src/session/session-storage.ts +70 -0
  92. package/src/session-import/claude.ts +382 -0
  93. package/src/session-import/codex.ts +458 -0
  94. package/src/session-import/command.ts +61 -0
  95. package/src/session-import/detect.ts +133 -0
  96. package/src/session-import/index.ts +27 -0
  97. package/src/session-import/redact.ts +125 -0
  98. package/src/session-import/service.ts +749 -0
  99. package/src/session-import/types.ts +163 -0
  100. package/src/skc-runtime/boot-generation.ts +172 -0
  101. package/src/skc-runtime/launch-tmux.ts +219 -41
  102. package/src/skc-runtime/launch-worktree.ts +277 -4
  103. package/src/skc-runtime/session-restore-runtime.ts +120 -0
  104. package/src/skc-runtime/session-restore.ts +296 -0
  105. package/src/skc-runtime/session-state-sidecar.ts +41 -0
  106. package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
  107. package/src/skc-runtime/tmux-sessions.ts +284 -108
  108. package/src/slash-commands/acp-builtins.ts +5 -1
  109. package/src/slash-commands/builtin-registry.ts +109 -5
  110. package/src/slash-commands/types.ts +5 -0
  111. package/src/tools/ask.ts +188 -10
  112. package/src/tools/eval.ts +2 -2
  113. package/src/export/html/template.css +0 -1060
  114. package/src/export/html/template.html +0 -47
  115. package/src/export/html/template.js +0 -2348
  116. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  117. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  118. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  119. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  120. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  121. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  122. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -4350,6 +4350,7 @@ export class SessionManager {
4350
4350
  #sessionName: string | undefined;
4351
4351
  #titleSource: "auto" | "user" | undefined;
4352
4352
  #sessionFile: string | undefined;
4353
+ #transcriptStatFailureReported = false;
4353
4354
  #flushed: boolean = false;
4354
4355
  #needsFullRewriteOnNextPersist: boolean = false;
4355
4356
  #ensuredOnDisk: boolean = false;
@@ -6573,6 +6574,28 @@ export class SessionManager {
6573
6574
  getSessionFile(): string | undefined {
6574
6575
  return this.#sessionFile;
6575
6576
  }
6577
+ /**
6578
+ * Managed on-disk transcript size in bytes. Returns undefined when the
6579
+ * destination is explicit, the file does not exist yet, or its identity
6580
+ * cannot be verified.
6581
+ */
6582
+ getTranscriptFileBytes(): number | undefined {
6583
+ if (this.destination.kind !== "managed" || !this.#sessionFile) return undefined;
6584
+ try {
6585
+ const size = this.#managedTranscriptStore().sizeSync(path.basename(this.#sessionFile));
6586
+ this.#transcriptStatFailureReported = false;
6587
+ return size ?? undefined;
6588
+ } catch (error) {
6589
+ if (!this.#transcriptStatFailureReported) {
6590
+ this.#transcriptStatFailureReported = true;
6591
+ logger.warn("Managed transcript size sampling failed", {
6592
+ sessionFile: this.#sessionFile,
6593
+ error: toError(error).message,
6594
+ });
6595
+ }
6596
+ return undefined;
6597
+ }
6598
+ }
6576
6599
 
6577
6600
  acquireMemoryGuardParticipantIngressLease(): MemoryGuardParticipantIngressLease {
6578
6601
  if (this.#memoryGuardParticipantIngressToken)
@@ -7093,7 +7116,17 @@ export class SessionManager {
7093
7116
  new Map(),
7094
7117
  );
7095
7118
  const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
7096
- this.#appendManagedRecordSync(persistedEntry);
7119
+ try {
7120
+ this.#appendManagedRecordSync(persistedEntry);
7121
+ } catch (appendError) {
7122
+ // A managed append-only transcript can hit the per-file limit
7123
+ // even after compaction reduced the live entries. Only the
7124
+ // authority-bound append error may authorize a full rewrite.
7125
+ if (!(appendError instanceof Error) || appendError.message !== "content_too_large") {
7126
+ throw appendError;
7127
+ }
7128
+ this.#rewriteFileSync();
7129
+ }
7097
7130
  return;
7098
7131
  }
7099
7132
  const writer = this.#ensurePersistWriter();
@@ -7119,16 +7152,29 @@ export class SessionManager {
7119
7152
  }
7120
7153
 
7121
7154
  #appendEntry(entry: SessionEntry): void {
7155
+ if (this.#persistError) throw this.#persistError;
7122
7156
  this.#assertRecoveryHydrationWritable();
7123
7157
  const normalizedEntry = normalizeSessionEntryForStorage(entry);
7124
7158
  const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
7159
+ const previousLeafId = this.#leafId;
7125
7160
  this.#fileEntries.push(residentEntry);
7126
7161
  this.#byId.set(residentEntry.id, residentEntry);
7127
7162
  this.#leafId = residentEntry.id;
7128
7163
  this.#bumpEntryRevision();
7129
7164
  this.#leafRevision++;
7130
7165
  if (entry.type === "label") this.#labelRevision++;
7131
- this._persist(residentEntry);
7166
+ try {
7167
+ this._persist(residentEntry);
7168
+ } catch (error) {
7169
+ this.#fileEntries.pop();
7170
+ this.#byId.delete(residentEntry.id);
7171
+ this.#leafId = previousLeafId;
7172
+ this.#entryRevision--;
7173
+ this.#leafRevision--;
7174
+ if (entry.type === "label") this.#labelRevision--;
7175
+ this.#resetMaterializedCaches();
7176
+ throw error;
7177
+ }
7132
7178
  // Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
7133
7179
  // malformed task-result or assistant usage shape reaching the append path must not
7134
7180
  // crash or poison getUsageStatistics() either.
@@ -9395,7 +9441,14 @@ export class SessionManager {
9395
9441
  */
9396
9442
  static inventorySessionsStrict(
9397
9443
  cwd: string,
9398
- options?: { sessionDir?: string; storage?: SessionStorage; destination?: SessionDestination },
9444
+ options?: {
9445
+ sessionDir?: string;
9446
+ storage?: SessionStorage;
9447
+ destination?: SessionDestination;
9448
+ maxCandidates?: number;
9449
+ maxTotalBytes?: number;
9450
+ filterOtherWorkspaces?: boolean;
9451
+ },
9399
9452
  ): StrictInventoryResult {
9400
9453
  const storage = options?.storage ?? new FileSessionStorage();
9401
9454
  const destination = options?.destination;
@@ -9419,20 +9472,27 @@ export class SessionManager {
9419
9472
  });
9420
9473
  if (resolved.kind === "error")
9421
9474
  return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
9422
- const listing = listManagedCandidates(resolved.scope);
9475
+ const listing = listManagedCandidates(resolved.scope, { maxCandidates: options?.maxCandidates });
9423
9476
  if (listing.kind === "error")
9424
9477
  return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
9425
9478
  const failures: StrictInventoryFailure[] = [];
9426
9479
  const candidates: StrictInventoryCandidate[] = [];
9480
+ let remainingBytes = options?.maxTotalBytes;
9427
9481
  for (const managedCandidate of listing.owned) {
9428
9482
  const candidate = inventoryReadCandidate(
9429
9483
  storage,
9430
9484
  managedCandidate.path,
9431
9485
  cwd,
9432
9486
  path.dirname(managedCandidate.path),
9487
+ remainingBytes,
9488
+ options?.filterOtherWorkspaces,
9433
9489
  );
9434
9490
  if ("failures" in candidate) failures.push(...candidate.failures);
9435
- else candidates.push(candidate.candidate);
9491
+ else {
9492
+ if (remainingBytes !== undefined) remainingBytes -= candidate.bytes;
9493
+ if ("filtered" in candidate) continue;
9494
+ candidates.push(candidate.candidate);
9495
+ }
9436
9496
  }
9437
9497
  assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
9438
9498
  store.assertBound();
@@ -9450,11 +9510,27 @@ export class SessionManager {
9450
9510
  failures: [{ kind: "scan", message: "Strict scoped session scan is unavailable", path: dir }],
9451
9511
  };
9452
9512
  }
9453
- const strictScan = storage.listFilesStrictSync.bind(storage);
9454
9513
 
9455
9514
  let files: string[];
9456
9515
  try {
9457
- files = strictScan(dir, "*.jsonl");
9516
+ if (options?.maxCandidates !== undefined) {
9517
+ if (!storage.listFilesStrictBoundedSync) {
9518
+ return {
9519
+ kind: "failure",
9520
+ failures: [{ kind: "scan", message: "Bounded strict session scan is unavailable", path: dir }],
9521
+ };
9522
+ }
9523
+ const bounded = storage.listFilesStrictBoundedSync(dir, "*.jsonl", options.maxCandidates);
9524
+ if (bounded.truncated) {
9525
+ return {
9526
+ kind: "failure",
9527
+ failures: [{ kind: "scan", message: "Strict session candidate limit exceeded", path: dir }],
9528
+ };
9529
+ }
9530
+ files = bounded.files;
9531
+ } else {
9532
+ files = storage.listFilesStrictSync(dir, "*.jsonl");
9533
+ }
9458
9534
  } catch (err) {
9459
9535
  const code = (err as NodeJS.ErrnoException)?.code;
9460
9536
  // Only a confirmed ENOENT proves the scoped root is genuinely absent,
@@ -9487,12 +9563,22 @@ export class SessionManager {
9487
9563
 
9488
9564
  const failures: StrictInventoryFailure[] = [];
9489
9565
  const candidates: StrictInventoryCandidate[] = [];
9566
+ let remainingBytes = options?.maxTotalBytes;
9490
9567
  for (const file of files) {
9491
- const candidate = inventoryReadCandidate(storage, file, cwd, dir);
9568
+ const candidate = inventoryReadCandidate(
9569
+ storage,
9570
+ file,
9571
+ cwd,
9572
+ dir,
9573
+ remainingBytes,
9574
+ options?.filterOtherWorkspaces,
9575
+ );
9492
9576
  if ("failures" in candidate) {
9493
9577
  failures.push(...candidate.failures);
9494
9578
  continue;
9495
9579
  }
9580
+ if (remainingBytes !== undefined) remainingBytes -= candidate.bytes;
9581
+ if ("filtered" in candidate) continue;
9496
9582
  candidates.push(candidate.candidate);
9497
9583
  }
9498
9584
  if (failures.length > 0) {
@@ -9521,7 +9607,12 @@ function inventoryReadCandidate(
9521
9607
  file: string,
9522
9608
  expectedCwd: string,
9523
9609
  sessionDir: string,
9524
- ): { candidate: StrictInventoryCandidate } | { failures: StrictInventoryFailure[] } {
9610
+ maxBytes?: number,
9611
+ filterOtherWorkspaces = false,
9612
+ ):
9613
+ | { candidate: StrictInventoryCandidate; bytes: number }
9614
+ | { filtered: true; bytes: number }
9615
+ | { failures: StrictInventoryFailure[] } {
9525
9616
  const failures: StrictInventoryFailure[] = [];
9526
9617
  const resolvedFile = path.resolve(file);
9527
9618
  if (!pathIsWithin(path.resolve(sessionDir), resolvedFile)) {
@@ -9534,7 +9625,14 @@ function inventoryReadCandidate(
9534
9625
  }
9535
9626
  let snapshot: SessionStorageSnapshot;
9536
9627
  try {
9537
- snapshot = storage.readSnapshotSync(file);
9628
+ if (maxBytes !== undefined) {
9629
+ if (maxBytes < 0 || !storage.readSnapshotBoundedSync) {
9630
+ return { failures: [{ kind: "read", message: "Bounded exact session read is unavailable", path: file }] };
9631
+ }
9632
+ snapshot = storage.readSnapshotBoundedSync(file, maxBytes);
9633
+ } else {
9634
+ snapshot = storage.readSnapshotSync(file);
9635
+ }
9538
9636
  } catch (err) {
9539
9637
  const code = (err as NodeJS.ErrnoException)?.code;
9540
9638
  if (code === "ELOOP" || code === "SYMLINK") {
@@ -9576,8 +9674,12 @@ function inventoryReadCandidate(
9576
9674
  }
9577
9675
  const canonicalHeaderCwd = resolveEquivalentPath(header.cwd);
9578
9676
  if (canonicalHeaderCwd !== resolveEquivalentPath(expectedCwd)) {
9677
+ if (filterOtherWorkspaces) return { filtered: true, bytes: snapshot.bytes.byteLength };
9579
9678
  failures.push({ kind: "cwd", message: "Candidate cwd does not match the scoped workspace", path: file });
9580
9679
  return { failures };
9581
9680
  }
9582
- return { candidate: { path: resolvedFile, id: header.id, cwd: header.cwd, identity: snapshot.stat } };
9681
+ return {
9682
+ candidate: { path: resolvedFile, id: header.id, cwd: header.cwd, identity: snapshot.stat },
9683
+ bytes: snapshot.bytes.byteLength,
9684
+ };
9583
9685
  }
@@ -165,6 +165,8 @@ export interface SessionStorage {
165
165
  readBytesSync?(path: string): Uint8Array;
166
166
  /** Exact bytes and descriptor-bound identity captured from one opened regular file. */
167
167
  readSnapshotSync?(path: string): SessionStorageSnapshot;
168
+ /** Exact descriptor-bound bytes, refusing growth or allocation beyond maxBytes. */
169
+ readSnapshotBoundedSync?(path: string, maxBytes: number): SessionStorageSnapshot;
168
170
  statSync(path: string): SessionStorageStat;
169
171
  listFilesSync(dir: string, pattern: string): string[];
170
172
  /** List matching files with mtimes without issuing one JavaScript stat call per path. */
@@ -174,6 +176,8 @@ export interface SessionStorage {
174
176
  * authorization inventory; the forgiving {@link listFilesSync} stays display-only.
175
177
  */
176
178
  listFilesStrictSync?(dir: string, pattern: string): string[];
179
+ /** Strict bounded scan; `truncated` means completeness was not granted. */
180
+ listFilesStrictBoundedSync?(dir: string, pattern: string, maxFiles: number): { files: string[]; truncated: boolean };
177
181
 
178
182
  exists(path: string): Promise<boolean>;
179
183
  readText(path: string): Promise<string>;
@@ -696,6 +700,39 @@ export class FileSessionStorage implements SessionStorage {
696
700
  }
697
701
  }
698
702
 
703
+ readSnapshotBoundedSync(fpath: string, maxBytes: number): SessionStorageSnapshot {
704
+ const flags = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0);
705
+ const fd = fs.openSync(fpath, flags);
706
+ try {
707
+ const initial = statFromNode(fs.fstatSync(fd, { bigint: true }));
708
+ if (!initial.isFile) throw new Error(`Not a regular file: ${fpath}`);
709
+ if (initial.size > maxBytes) throw new Error("Snapshot exceeds the strict byte limit.");
710
+ const chunks: Buffer[] = [];
711
+ let total = 0;
712
+ while (total <= maxBytes) {
713
+ const chunk = Buffer.allocUnsafe(Math.min(1024 * 1024, maxBytes - total + 1));
714
+ const bytesRead = fs.readSync(fd, chunk, 0, chunk.byteLength, null);
715
+ if (bytesRead === 0) break;
716
+ total += bytesRead;
717
+ if (total > maxBytes) throw new Error("Snapshot exceeds the strict byte limit.");
718
+ chunks.push(chunk.subarray(0, bytesRead));
719
+ }
720
+ const terminal = statFromNode(fs.fstatSync(fd, { bigint: true }));
721
+ if (
722
+ initial.dev !== terminal.dev ||
723
+ initial.ino !== terminal.ino ||
724
+ initial.size !== terminal.size ||
725
+ initial.mtimeNs !== terminal.mtimeNs ||
726
+ total !== terminal.size
727
+ ) {
728
+ throw new Error("Snapshot changed during the bounded read.");
729
+ }
730
+ return { bytes: Buffer.concat(chunks, total), stat: terminal };
731
+ } finally {
732
+ fs.closeSync(fd);
733
+ }
734
+ }
735
+
699
736
  statSync(path: string): SessionStorageStat {
700
737
  return statFromNode(fs.statSync(path, { bigint: true }));
701
738
  }
@@ -729,6 +766,26 @@ export class FileSessionStorage implements SessionStorage {
729
766
  return Array.from(new Bun.Glob(pattern).scanSync(dir)).map(name => path.join(dir, name));
730
767
  }
731
768
 
769
+ listFilesStrictBoundedSync(dir: string, pattern: string, maxFiles: number): { files: string[]; truncated: boolean } {
770
+ const handle = fs.opendirSync(dir);
771
+ const files: string[] = [];
772
+ let entries = 0;
773
+ try {
774
+ for (;;) {
775
+ const entry = handle.readSync();
776
+ if (!entry) break;
777
+ entries++;
778
+ if (entries > maxFiles * 4 + 128) return { files, truncated: true };
779
+ if (!matchesPattern(entry.name, pattern)) continue;
780
+ files.push(path.join(dir, entry.name));
781
+ if (files.length > maxFiles) return { files, truncated: true };
782
+ }
783
+ return { files, truncated: false };
784
+ } finally {
785
+ handle.closeSync();
786
+ }
787
+ }
788
+
732
789
  async exists(path: string): Promise<boolean> {
733
790
  try {
734
791
  await fs.promises.access(path);
@@ -1469,6 +1526,12 @@ export class MemorySessionStorage implements SessionStorage {
1469
1526
  return { bytes: Buffer.from(entry.content), stat: this.#statFor(entry) };
1470
1527
  }
1471
1528
 
1529
+ readSnapshotBoundedSync(path: string, maxBytes: number): SessionStorageSnapshot {
1530
+ const snapshot = this.readSnapshotSync(path);
1531
+ if (snapshot.bytes.byteLength > maxBytes) throw new Error("Snapshot exceeds the strict byte limit.");
1532
+ return snapshot;
1533
+ }
1534
+
1472
1535
  statSync(path: string): SessionStorageStat {
1473
1536
  const entry = this.#files.get(path);
1474
1537
  if (!entry) throw new Error(`File not found: ${path}`);
@@ -1498,6 +1561,13 @@ export class MemorySessionStorage implements SessionStorage {
1498
1561
  return this.listFilesSync(dir, pattern);
1499
1562
  }
1500
1563
 
1564
+ listFilesStrictBoundedSync(dir: string, pattern: string, maxFiles: number): { files: string[]; truncated: boolean } {
1565
+ const files = this.listFilesSync(dir, pattern);
1566
+ return files.length > maxFiles
1567
+ ? { files: files.slice(0, maxFiles + 1), truncated: true }
1568
+ : { files, truncated: false };
1569
+ }
1570
+
1501
1571
  exists(path: string): Promise<boolean> {
1502
1572
  return Promise.resolve(this.existsSync(path));
1503
1573
  }