@wrongstack/core 0.303.0 → 0.305.1

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 (76) hide show
  1. package/dist/chronicle/project-server.js +18 -48
  2. package/dist/coordination/agents/index.js +530 -130
  3. package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
  4. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  5. package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
  6. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  7. package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
  8. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  9. package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
  10. package/dist/coordination/agents/types.d.ts +10 -2
  11. package/dist/coordination/director-prompts.d.ts +19 -6
  12. package/dist/coordination/director-tools.d.ts +2 -2
  13. package/dist/coordination/fleet.d.ts +0 -6
  14. package/dist/coordination/index.d.ts +2 -1
  15. package/dist/coordination/index.js +1581 -955
  16. package/dist/coordination/mailbox-project-server.js +28 -57
  17. package/dist/core/agent-types.d.ts +4 -2
  18. package/dist/core/agent.d.ts +1 -0
  19. package/dist/core/context.d.ts +15 -0
  20. package/dist/core/conversation-state.d.ts +14 -0
  21. package/dist/core/fallback-profile-manager.d.ts +70 -2
  22. package/dist/core/index.js +308 -108
  23. package/dist/core/system-prompt-blocks.d.ts +1 -1
  24. package/dist/core/system-prompt-builder.d.ts +13 -1
  25. package/dist/core/system-prompt-glossary.d.ts +73 -0
  26. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  27. package/dist/defaults/index.js +910 -693
  28. package/dist/execution/council-orchestrator.d.ts +3 -13
  29. package/dist/execution/index.js +211 -75
  30. package/dist/execution/one-shot-llm.d.ts +5 -0
  31. package/dist/hq/index.js +17 -7
  32. package/dist/hq/protocol/kanban.d.ts +21 -0
  33. package/dist/hq/protocol.js +5 -1
  34. package/dist/hq/redaction.d.ts +14 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +3505 -2539
  37. package/dist/infrastructure/index.js +247 -122
  38. package/dist/plugin/index.js +101 -3
  39. package/dist/registry/index.js +11 -0
  40. package/dist/registry/tool-registry.d.ts +8 -0
  41. package/dist/replay/hash.d.ts +9 -0
  42. package/dist/replay/index.js +14 -4
  43. package/dist/replay/replay-provider-runner.d.ts +31 -1
  44. package/dist/security/index.js +25 -20
  45. package/dist/security/secret-vault.d.ts +2 -0
  46. package/dist/session-catalog/index.js +62 -8
  47. package/dist/session-catalog/project-server.js +109 -78
  48. package/dist/session-catalog/protocol.d.ts +11 -4
  49. package/dist/session-catalog/store.d.ts +2 -2
  50. package/dist/storage/index.js +224 -67
  51. package/dist/storage/memory-consolidator.d.ts +4 -2
  52. package/dist/storage/session-resume-validation.d.ts +24 -0
  53. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  54. package/dist/storage/session-store/fork-session.d.ts +13 -1
  55. package/dist/storage/session-store/load-cache.d.ts +11 -0
  56. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  57. package/dist/storage/session-store.d.ts +18 -0
  58. package/dist/tools/index.js +174 -74
  59. package/dist/types/config/mcp-features.d.ts +31 -1
  60. package/dist/types/config/root.d.ts +12 -0
  61. package/dist/types/config/tools.d.ts +22 -0
  62. package/dist/types/config/ui.d.ts +7 -4
  63. package/dist/types/default-config.d.ts +1 -0
  64. package/dist/types/index.js +24 -1
  65. package/dist/types/session.d.ts +9 -1
  66. package/dist/utils/index.d.ts +1 -0
  67. package/dist/utils/index.js +214 -76
  68. package/dist/utils/project-state-guard.d.ts +21 -0
  69. package/dist/utils/session-scoped-path.d.ts +17 -0
  70. package/dist/utils/todos-format.d.ts +20 -0
  71. package/instructions/leader-after-task.md +3 -4
  72. package/instructions/system-lite.md +10 -13
  73. package/instructions/system-pro.md +18 -25
  74. package/instructions/system.md +18 -23
  75. package/package.json +3 -3
  76. package/skills/wrongstack-kanban/SKILL.md +95 -124
@@ -2443,12 +2443,97 @@ var FallbackProfileManager = class {
2443
2443
  return this.resolveRefs([ref], exclude);
2444
2444
  }
2445
2445
  /**
2446
- * Resolve every usable configured target as an uncapped last-resort chain.
2446
+ * Resolve every usable configured target as a bounded last-resort chain.
2447
2447
  * Normal smart defaults stay bounded; callers append this only after the
2448
- * preferred chain and only when automatic fallback is enabled.
2448
+ * preferred chain and only when automatic fallback is enabled. The cap
2449
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
2450
+ * from producing a degenerate chain of doomed requests during a systemic
2451
+ * outage — by this point the smart default, bridge, and default profile
2452
+ * have already failed.
2449
2453
  */
2450
2454
  resolveAllConfigured(exclude) {
2451
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
2455
+ return this.smartDefault(exclude, this.lastResortCap());
2456
+ }
2457
+ /**
2458
+ * Effective cap for the last-resort append. Reads the user-configurable
2459
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
2460
+ * otherwise falls back to the compiled-in default
2461
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
2462
+ */
2463
+ lastResortCap() {
2464
+ const configured = this.config.fallbackMaxLastResortCandidates;
2465
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
2466
+ return Math.floor(configured);
2467
+ }
2468
+ return MAX_LAST_RESORT_CANDIDATES;
2469
+ }
2470
+ /**
2471
+ * Build the complete fallback candidate chain shared by the agent-loop
2472
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
2473
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
2474
+ * gate so both consumers produce identical ordering and depth semantics.
2475
+ *
2476
+ * Layering (each step deduped against all prior):
2477
+ * 1. Bridge (emergency continuity route).
2478
+ * 2. Configured primary, when the live context drifted from it.
2479
+ * 3. The selected chain (explicit refs → named profile → smart default),
2480
+ * via {@link resolveEffective}.
2481
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
2482
+ * 5. Every other configured provider — last resort, ONLY when the chain was
2483
+ * auto-derived AND `effectiveFallbackAuto` is true.
2484
+ *
2485
+ * Returns the empty chain when `closedWorld` is true and no explicit
2486
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
2487
+ *
2488
+ * @internal caller-aware options (primary, closedWorld) are accepted because
2489
+ * the agent loop has context the manager does not own; the resolution
2490
+ * pipeline itself is identical for both callers.
2491
+ */
2492
+ resolveCandidates(current, opts = {}) {
2493
+ const configuredPrimary = opts.primary ?? {
2494
+ providerId: this.config.provider,
2495
+ model: this.config.model
2496
+ };
2497
+ const configFallbackAuto = this.config.fallbackAuto;
2498
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
2499
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
2500
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
2501
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
2502
+ const fromExplicitSource = explicitUsable || profileUsable;
2503
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2504
+ fallbackModels: explicitRefs,
2505
+ fallbackProfile: opts.fallbackProfile,
2506
+ fallbackAuto: effectiveFallbackAuto,
2507
+ exclude: current
2508
+ });
2509
+ const candidates = [];
2510
+ if (opts.closedWorld) {
2511
+ candidates.push(...selectedChain);
2512
+ } else {
2513
+ candidates.push(...this.resolveBridge(current));
2514
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
2515
+ candidates.push({
2516
+ providerId: configuredPrimary.providerId,
2517
+ model: configuredPrimary.model,
2518
+ providerSwitched: configuredPrimary.providerId !== current.providerId
2519
+ });
2520
+ }
2521
+ candidates.push(...selectedChain);
2522
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
2523
+ candidates.push(...this.resolve("default", { exclude: current }));
2524
+ }
2525
+ if (!fromExplicitSource && effectiveFallbackAuto) {
2526
+ const cap = this.lastResortCap();
2527
+ if (cap > 0) {
2528
+ const usedKeys = new Set(
2529
+ candidates.map((c) => `${c.providerId}/${c.model}`)
2530
+ );
2531
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
2532
+ candidates.push(...lastResort);
2533
+ }
2534
+ }
2535
+ }
2536
+ return dedupeChain(candidates, current);
2452
2537
  }
2453
2538
  // ── Provider availability (read-only) ──────────────────────────────────
2454
2539
  checkProvider(providerId) {
@@ -2572,7 +2657,20 @@ var FallbackProfileManager = class {
2572
2657
  );
2573
2658
  }
2574
2659
  };
2660
+ var MAX_LAST_RESORT_CANDIDATES = 12;
2575
2661
  var FREEZER_EMPTY = Object.freeze([]);
2662
+ function dedupeChain(entries, current) {
2663
+ const seen = /* @__PURE__ */ new Set();
2664
+ const currentKey = `${current.providerId}/${current.model}`;
2665
+ return Object.freeze(
2666
+ entries.filter((entry) => {
2667
+ const key = `${entry.providerId}/${entry.model}`;
2668
+ if (key === currentKey || seen.has(key)) return false;
2669
+ seen.add(key);
2670
+ return true;
2671
+ })
2672
+ );
2673
+ }
2576
2674
 
2577
2675
  // src/plugins/auto-review-plugin.ts
2578
2676
  init_error();
@@ -759,6 +759,17 @@ var ToolRegistry = class _ToolRegistry {
759
759
  this._providerListSnapshotVersion = this._version;
760
760
  return arr;
761
761
  }
762
+ /**
763
+ * Whether an enabled catalog tool is part of the direct provider surface.
764
+ *
765
+ * This is intentionally different from `get()`/`list()`: a tool can remain
766
+ * executable through a lazy gateway while its schema is omitted from the
767
+ * next provider request. Disabled and unknown tools always return false.
768
+ */
769
+ isExposedToProvider(name) {
770
+ if (this._disabled.has(name) || !this.tools.has(name)) return false;
771
+ return this._providerToolNames?.has(name) ?? true;
772
+ }
762
773
  /**
763
774
  * Group tools by their `category` field. Tools without a category
764
775
  * are placed under the key `""` (empty string). Returns a Map of
@@ -149,6 +149,14 @@ export declare class ToolRegistry {
149
149
  exposeToProvider(names: string | readonly string[]): void;
150
150
  /** Tools serialized into provider requests and described by the system prompt. */
151
151
  listForProvider(): Tool[];
152
+ /**
153
+ * Whether an enabled catalog tool is part of the direct provider surface.
154
+ *
155
+ * This is intentionally different from `get()`/`list()`: a tool can remain
156
+ * executable through a lazy gateway while its schema is omitted from the
157
+ * next provider request. Disabled and unknown tools always return false.
158
+ */
159
+ isExposedToProvider(name: string): boolean;
152
160
  /**
153
161
  * Group tools by their `category` field. Tools without a category
154
162
  * are placed under the key `""` (empty string). Returns a Map of
@@ -17,6 +17,15 @@ import type { Request } from '../types/provider.js';
17
17
  * sampling knobs (`temperature`, `topP`, `stopSequences`,
18
18
  * `toolChoice`). Anything else on the `Request` (metadata,
19
19
  * future extensions) is ignored so replay stays forward-compat.
20
+ * - The same rule applies *inside* each message. `Request.messages`
21
+ * is the live `ctx.messages` array, whose entries carry local
22
+ * bookkeeping the provider adapters explicitly drop: `ts` (wall
23
+ * clock at the moment the turn was created), `_estTokens` (a
24
+ * mutation-time cache) and `origin`. Hashing those made every
25
+ * hash a function of when the run happened, so a recorded
26
+ * response could never be found again and `mode: 'replay'` threw
27
+ * on the first call it was asked to serve. See
28
+ * {@link semanticMessage}.
20
29
  * - We serialize to JSON. The `ContentBlock` and `Message` shapes
21
30
  * are pure data; this works as long as no `undefined` values
22
31
  * sneak in (those get dropped by `JSON.stringify`, which is
@@ -15,11 +15,15 @@ function sortKeys(value) {
15
15
  }
16
16
  return value;
17
17
  }
18
+ function semanticMessage(message) {
19
+ const { ts: _ts, _estTokens: _estimate, origin: _origin, ...semantic } = message;
20
+ return semantic;
21
+ }
18
22
  function hashRequest(request) {
19
23
  const payload = {
20
24
  model: request.model,
21
25
  system: request.system,
22
- messages: request.messages,
26
+ messages: request.messages.map(semanticMessage),
23
27
  tools: request.tools,
24
28
  maxTokens: request.maxTokens,
25
29
  temperature: request.temperature,
@@ -40,16 +44,22 @@ var ReplayProviderRunner = class {
40
44
  }
41
45
  inner;
42
46
  opts;
47
+ /** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
48
+ resolveSessionId() {
49
+ const { sessionId } = this.opts;
50
+ return typeof sessionId === "function" ? sessionId() : sessionId;
51
+ }
43
52
  async run(runOpts) {
44
53
  const hash = hashRequest(runOpts.request);
45
- const cached = await this.opts.log.lookup(this.opts.sessionId, hash);
54
+ const sessionId = this.resolveSessionId();
55
+ const cached = await this.opts.log.lookup(sessionId, hash);
46
56
  if (this.opts.mode === "replay") {
47
57
  if (!cached) {
48
58
  this.opts.logger?.warn?.(
49
59
  `replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
50
60
  );
51
61
  throw new Error(
52
- `ReplayProviderRunner: no recorded response for hash ${hash} in session ${this.opts.sessionId}. Either the request changed since recording, or this session has no replay log.`
62
+ `ReplayProviderRunner: no recorded response for hash ${hash} in session ${sessionId}. Either the request changed since recording, or this session has no replay log.`
53
63
  );
54
64
  }
55
65
  this.opts.logger?.debug?.(
@@ -65,7 +75,7 @@ var ReplayProviderRunner = class {
65
75
  }
66
76
  const response = await this.inner.run(runOpts);
67
77
  await this.opts.log.record({
68
- sessionId: this.opts.sessionId,
78
+ sessionId,
69
79
  request: runOpts.request,
70
80
  response
71
81
  });
@@ -26,11 +26,39 @@ import type { Response } from '../types/provider.js';
26
26
  * a particular request is seen, it's recorded; the next time
27
27
  * the same request is made, the recorded response is served.
28
28
  * This is the cheapest path to a deterministic test harness.
29
+ *
30
+ * ## What a hit requires
31
+ *
32
+ * The lookup key is `hashRequest`, which covers everything the provider is
33
+ * actually sent — including `system`. The agent loop appends volatile blocks
34
+ * to the system prompt on every request (the continuity ledger, the live
35
+ * next-steps gate, memory evidence; see `agent-response.ts`), and those blocks
36
+ * describe the state of *this* run. A re-run whose ledger or retrieved memory
37
+ * differs therefore produces a different request, and correctly misses: the
38
+ * recorded response answered a different prompt.
39
+ *
40
+ * So `mode: 'replay'` reproduces a run faithfully only when the loop is driven
41
+ * back through the same states — which is what a test harness does, and what a
42
+ * fresh interactive session does not. In an interactive session, prefer
43
+ * `'record'` (build the log) and use `'replay'` from a harness that pins the
44
+ * volatile inputs. `hashRequest` itself is stable across time: local message
45
+ * bookkeeping (`ts`, `_estTokens`, `origin`) is stripped before hashing.
29
46
  */
30
47
  export type ReplayMode = 'record' | 'replay' | 'auto';
31
48
  export interface ReplayProviderRunnerOptions {
32
49
  log: ReplayLogStore;
33
- sessionId: string;
50
+ /**
51
+ * Which session's log to read and write.
52
+ *
53
+ * Accepts a resolver because recording is wired before the session exists:
54
+ * the boot path binds this runner while the session writer is still being
55
+ * created, and the user may then swap sessions (`/resume`, new session)
56
+ * without the runner being rebuilt. A frozen string there meant the log was
57
+ * filed under a synthetic id with no session behind it. A resolver keeps the
58
+ * log next to the transcript it belongs to, for the session that is actually
59
+ * live at the time of the call.
60
+ */
61
+ sessionId: string | (() => string);
34
62
  mode: ReplayMode;
35
63
  /**
36
64
  * Optional logger — receives a debug line on every replay hit
@@ -48,6 +76,8 @@ export declare class ReplayProviderRunner implements ProviderRunner {
48
76
  private readonly inner;
49
77
  private readonly opts;
50
78
  constructor(inner: ProviderRunner, opts: ReplayProviderRunnerOptions);
79
+ /** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
80
+ private resolveSessionId;
51
81
  run(runOpts: RunProviderOptions): Promise<Response>;
52
82
  }
53
83
  //# sourceMappingURL=replay-provider-runner.d.ts.map
@@ -1349,7 +1349,7 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
1349
1349
  } : { decision: "allow" };
1350
1350
  }
1351
1351
  const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
1352
- if (governanceRequired) {
1352
+ if (governanceRequired && board.lifecycle?.mode === "managed") {
1353
1353
  if (!identity.taskId) {
1354
1354
  return {
1355
1355
  decision: "block",
@@ -1375,9 +1375,11 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
1375
1375
  };
1376
1376
  }
1377
1377
  if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
1378
+ const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
1379
+ const assignmentStatus = task.assignment?.status ?? "missing";
1378
1380
  return {
1379
1381
  decision: "block",
1380
- reason: "Active card must be in Running with a live assignment before product mutation. Call kanban start_task after completing the required card details.",
1382
+ reason: `Active card must be in Running with a live assignment before product mutation (lifecycle: ${lifecycleStage}; assignment: ${assignmentStatus}). Call kanban start_task after completing the required card details.`,
1381
1383
  boardId: board.id,
1382
1384
  taskId: task.id
1383
1385
  };
@@ -3607,23 +3609,21 @@ function unwrapDataKey(buf, keyFile) {
3607
3609
  });
3608
3610
  }
3609
3611
  }
3610
- function checkKeyFilePermissions(keyFile, opts) {
3611
- if (process.platform === "win32") return;
3612
+ function keyFileNeedsHardening(keyFile, opts) {
3613
+ if (process.platform === "win32") return false;
3612
3614
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
3613
3615
  try {
3614
3616
  const stat = fs2.statSync(keyFile);
3615
3617
  const actualMode = stat.mode & 511;
3616
3618
  if (actualMode !== KEY_FILE_MODE) {
3617
- void restrictFilePermissions(keyFile, {
3618
- label: "secret-vault",
3619
- warn
3620
- }).catch(() => void 0);
3621
3619
  warn(
3622
3620
  `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
3623
3621
  );
3622
+ return true;
3624
3623
  }
3625
3624
  } catch {
3626
3625
  }
3626
+ return false;
3627
3627
  }
3628
3628
  function writeKeyFileAtomicSync(keyFile, content) {
3629
3629
  const tmp = `${keyFile}.${randomBytes2(4).toString("hex")}.tmp`;
@@ -3689,6 +3689,14 @@ var DefaultSecretVault = class {
3689
3689
  }).catch(() => void 0);
3690
3690
  this.pendingHardening.push(p);
3691
3691
  }
3692
+ /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
3693
+ checkKeyFilePermissions() {
3694
+ if (keyFileNeedsHardening(this.keyFile, {
3695
+ warn: (msg) => this.logWarn(msg)
3696
+ })) {
3697
+ this.scheduleKeyHardening();
3698
+ }
3699
+ }
3692
3700
  /** Flush all pending key-file hardening promises. */
3693
3701
  flushHardening() {
3694
3702
  if (this.pendingHardening.length === 0) return Promise.resolve();
@@ -3776,7 +3784,7 @@ var DefaultSecretVault = class {
3776
3784
  writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
3777
3785
  }
3778
3786
  this.scheduleKeyHardening();
3779
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3787
+ this.checkKeyFilePermissions();
3780
3788
  this.key = newKey;
3781
3789
  this._keyVersion = newVersion;
3782
3790
  return { oldVersion, newVersion };
@@ -3792,12 +3800,9 @@ var DefaultSecretVault = class {
3792
3800
  const passphrase = getVaultPassphrase();
3793
3801
  if (!passphrase || !this.key) return;
3794
3802
  try {
3795
- writeKeyFileAtomicSync(
3796
- this.keyFile,
3797
- wrapDataKey(this.key, this._keyVersion, passphrase)
3798
- );
3803
+ writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
3799
3804
  this.scheduleKeyHardening();
3800
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3805
+ this.checkKeyFilePermissions();
3801
3806
  } catch {
3802
3807
  }
3803
3808
  }
@@ -3809,13 +3814,13 @@ var DefaultSecretVault = class {
3809
3814
  const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
3810
3815
  this.key = key2;
3811
3816
  this._keyVersion = version;
3812
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3817
+ this.checkKeyFilePermissions();
3813
3818
  return this.key;
3814
3819
  }
3815
3820
  if (buf.length === KEY_BYTES) {
3816
3821
  this.key = buf;
3817
3822
  this._keyVersion = 1;
3818
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3823
+ this.checkKeyFilePermissions();
3819
3824
  this.migrateToWrappedIfPassphrase();
3820
3825
  return this.key;
3821
3826
  }
@@ -3839,7 +3844,7 @@ var DefaultSecretVault = class {
3839
3844
  }
3840
3845
  this.key = Buffer.from(key2);
3841
3846
  this._keyVersion = version;
3842
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3847
+ this.checkKeyFilePermissions();
3843
3848
  this.migrateToWrappedIfPassphrase();
3844
3849
  return this.key;
3845
3850
  }
@@ -3866,13 +3871,13 @@ var DefaultSecretVault = class {
3866
3871
  const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
3867
3872
  this.key = winnerKey;
3868
3873
  this._keyVersion = version;
3869
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3874
+ this.checkKeyFilePermissions();
3870
3875
  return this.key;
3871
3876
  }
3872
3877
  if (buf.length === KEY_BYTES) {
3873
3878
  this.key = buf;
3874
3879
  this._keyVersion = 1;
3875
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3880
+ this.checkKeyFilePermissions();
3876
3881
  return this.key;
3877
3882
  }
3878
3883
  if (buf.length === VERSIONED_KEY_FILE_SIZE) {
@@ -3888,7 +3893,7 @@ var DefaultSecretVault = class {
3888
3893
  const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
3889
3894
  this.key = Buffer.from(winnerKey);
3890
3895
  this._keyVersion = version;
3891
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3896
+ this.checkKeyFilePermissions();
3892
3897
  return this.key;
3893
3898
  }
3894
3899
  throw new ConfigError({
@@ -46,6 +46,8 @@ export declare class DefaultSecretVault implements RotatableSecretVault {
46
46
  * can never become an unhandled one.
47
47
  */
48
48
  private scheduleKeyHardening;
49
+ /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
50
+ private checkKeyFilePermissions;
49
51
  /** Flush all pending key-file hardening promises. */
50
52
  flushHardening(): Promise<void>;
51
53
  /** Current key version. Starts at 1; incremented by rotateKey(). */
@@ -1198,6 +1198,22 @@ function isPidAlive(pid) {
1198
1198
  }
1199
1199
  }
1200
1200
 
1201
+ // src/utils/session-scoped-path.ts
1202
+ var SESSION_SIDECAR_JSONL_SUFFIXES = [
1203
+ ".replay.jsonl",
1204
+ ".audit.jsonl",
1205
+ ".annotations.jsonl"
1206
+ ];
1207
+ var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
1208
+ "_index.jsonl",
1209
+ "_mailbox.jsonl"
1210
+ ]);
1211
+ function isSessionTranscriptFileName(name) {
1212
+ if (!name.endsWith(".jsonl")) return false;
1213
+ if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
1214
+ return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
1215
+ }
1216
+
1201
1217
  // src/session-catalog/store.ts
1202
1218
  var SCHEMA_VERSION = 1;
1203
1219
  var MAX_LEASE_MS = 12e4;
@@ -1683,13 +1699,51 @@ var SessionCatalogStore = class {
1683
1699
  damaged: row.damaged !== 0
1684
1700
  };
1685
1701
  }
1686
- listCatalog(limit = 100, search) {
1687
- const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(limit)));
1688
- const rows = search?.trim() ? this.db.prepare(
1689
- "SELECT * FROM sessions WHERE session_id LIKE ? OR json_extract(summary_json,'$.title') LIKE ? OR json_extract(summary_json,'$.name') LIKE ? ORDER BY COALESCE(json_extract(summary_json,'$.lastActivityAt'),json_extract(summary_json,'$.startedAt')) DESC LIMIT ?"
1690
- ).all(`%${search.trim()}%`, `%${search.trim()}%`, `%${search.trim()}%`, bounded) : this.db.prepare(
1691
- "SELECT * FROM sessions ORDER BY COALESCE(json_extract(summary_json,'$.lastActivityAt'),json_extract(summary_json,'$.startedAt')) DESC LIMIT ?"
1692
- ).all(bounded);
1702
+ listCatalog(criteria = {}) {
1703
+ const requestedLimit = criteria.limit ?? 100;
1704
+ if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
1705
+ const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
1706
+ const clauses = [];
1707
+ const values = [];
1708
+ const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
1709
+ const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
1710
+ const search = criteria.search?.trim();
1711
+ if (search) {
1712
+ const pattern = literalLike(search);
1713
+ clauses.push(
1714
+ `(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
1715
+ );
1716
+ values.push(pattern, pattern, pattern);
1717
+ }
1718
+ if (criteria.since) {
1719
+ clauses.push(`${jsonText("startedAt")}>=?`);
1720
+ values.push(criteria.since);
1721
+ }
1722
+ if (criteria.until) {
1723
+ clauses.push(`${jsonText("startedAt")}<=?`);
1724
+ values.push(criteria.until);
1725
+ }
1726
+ if (criteria.provider) {
1727
+ clauses.push(`${jsonText("provider")}=?`);
1728
+ values.push(criteria.provider);
1729
+ }
1730
+ if (criteria.model) {
1731
+ clauses.push(`${jsonText("model")}=?`);
1732
+ values.push(criteria.model);
1733
+ }
1734
+ if (criteria.minTokens !== void 0) {
1735
+ if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
1736
+ clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
1737
+ values.push(criteria.minTokens);
1738
+ }
1739
+ if (criteria.titleContains) {
1740
+ clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
1741
+ values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
1742
+ }
1743
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
1744
+ const rows = this.db.prepare(
1745
+ `SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
1746
+ ).all(...values, bounded);
1693
1747
  return rows.map((row) => this.catalogRecord(row));
1694
1748
  }
1695
1749
  getSummary(sessionId) {
@@ -1844,7 +1898,7 @@ var SessionCatalogStore = class {
1844
1898
  rebuildCatalog() {
1845
1899
  const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
1846
1900
  const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
1847
- (file) => !file.endsWith("_index.jsonl")
1901
+ (file) => isSessionTranscriptFileName(path4.basename(file))
1848
1902
  );
1849
1903
  const ids = /* @__PURE__ */ new Set();
1850
1904
  for (const file of [...summaries, ...transcripts]) {