@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
@@ -2152,15 +2152,51 @@ var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
2152
2152
  var ensureDir = primitives.ensureDir;
2153
2153
  var withFileLock = primitives.withFileLock;
2154
2154
 
2155
+ // src/utils/session-scoped-path.ts
2156
+ import * as path6 from "node:path";
2157
+ function sessionScopedPath(dir, sessionId, suffix) {
2158
+ if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
2159
+ throw invalid(sessionId);
2160
+ }
2161
+ const resolved = path6.resolve(dir, `${sessionId}${suffix}`);
2162
+ const rel = path6.relative(path6.resolve(dir), resolved);
2163
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) {
2164
+ throw invalid(sessionId);
2165
+ }
2166
+ return resolved;
2167
+ }
2168
+ var SESSION_SIDECAR_JSONL_SUFFIXES = [
2169
+ ".replay.jsonl",
2170
+ ".audit.jsonl",
2171
+ ".annotations.jsonl"
2172
+ ];
2173
+ var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
2174
+ "_index.jsonl",
2175
+ "_mailbox.jsonl"
2176
+ ]);
2177
+ function isSessionTranscriptFileName(name) {
2178
+ if (!name.endsWith(".jsonl")) return false;
2179
+ if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
2180
+ return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
2181
+ }
2182
+ function invalid(sessionId) {
2183
+ return new FsError({
2184
+ message: `Invalid sessionId: ${sessionId}`,
2185
+ code: ERROR_CODES.FS_DELETE_FAILED,
2186
+ path: sessionId,
2187
+ context: { reason: "path_traversal" }
2188
+ });
2189
+ }
2190
+
2155
2191
  // src/session-registry.ts
2156
2192
  import * as fs6 from "node:fs/promises";
2157
- import * as path7 from "node:path";
2193
+ import * as path8 from "node:path";
2158
2194
 
2159
2195
  // src/session-registry-atomic-file.ts
2160
2196
  import { randomUUID as randomUUID2 } from "node:crypto";
2161
2197
  import * as fs5 from "node:fs/promises";
2162
2198
  import { hostname } from "node:os";
2163
- import * as path6 from "node:path";
2199
+ import * as path7 from "node:path";
2164
2200
  var STALE_LOCK_MS = 1e4;
2165
2201
  var SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
2166
2202
  var STALE_TMP_MS = 6e4;
@@ -2240,9 +2276,9 @@ async function breakLockAtomically(lockPath) {
2240
2276
  return true;
2241
2277
  }
2242
2278
  async function writeAtomicFile(filePath, registry) {
2243
- const tmp = path6.join(
2244
- path6.dirname(filePath),
2245
- `.${path6.basename(filePath)}.${randomUUID2().slice(0, 8)}.tmp`
2279
+ const tmp = path7.join(
2280
+ path7.dirname(filePath),
2281
+ `.${path7.basename(filePath)}.${randomUUID2().slice(0, 8)}.tmp`
2246
2282
  );
2247
2283
  let tmpPersisted = false;
2248
2284
  try {
@@ -2283,21 +2319,21 @@ async function writeAtomicFile(filePath, registry) {
2283
2319
  }
2284
2320
  async function pruneStaleTempFiles(filePath) {
2285
2321
  try {
2286
- const dir = path6.dirname(filePath);
2287
- const base = path6.basename(filePath);
2322
+ const dir = path7.dirname(filePath);
2323
+ const base = path7.basename(filePath);
2288
2324
  const now = Date.now();
2289
2325
  const stale = [];
2290
2326
  for (const name of await fs5.readdir(dir)) {
2291
2327
  const isTemp = (name.startsWith(`${base}.`) || name.startsWith(`.${base}.`)) && name.endsWith(".tmp");
2292
2328
  if (!isTemp) continue;
2293
- const stat15 = await fs5.stat(path6.join(dir, name)).catch(() => null);
2329
+ const stat15 = await fs5.stat(path7.join(dir, name)).catch(() => null);
2294
2330
  if (!stat15) continue;
2295
2331
  if (now - stat15.mtimeMs > STALE_TMP_MS) stale.push({ name, mtimeMs: stat15.mtimeMs });
2296
2332
  }
2297
2333
  stale.sort((a, b) => b.mtimeMs - a.mtimeMs);
2298
2334
  await Promise.all(
2299
2335
  stale.slice(MAX_STALE_TMP_FILES).map(async ({ name }) => {
2300
- await fs5.unlink(path6.join(dir, name)).catch(() => void 0);
2336
+ await fs5.unlink(path7.join(dir, name)).catch(() => void 0);
2301
2337
  })
2302
2338
  );
2303
2339
  } catch {
@@ -2361,7 +2397,7 @@ var SessionRegistry = class {
2361
2397
  lastEntry = null;
2362
2398
  ownershipLockWaitMs;
2363
2399
  constructor(globalRoot, options = {}) {
2364
- this.filePath = path7.join(globalRoot, REGISTRY_FILE);
2400
+ this.filePath = path8.join(globalRoot, REGISTRY_FILE);
2365
2401
  this.ownershipLockWaitMs = Math.max(0, options.ownershipLockWaitMs ?? OWNERSHIP_LOCK_WAIT_MS);
2366
2402
  }
2367
2403
  // ── Public API ──────────────────────────────────────────────────────────
@@ -2671,7 +2707,7 @@ var SessionRegistry = class {
2671
2707
  const deadline = Date.now() + waitBudgetMs;
2672
2708
  let attempt = 0;
2673
2709
  try {
2674
- await fs6.mkdir(path7.dirname(this.filePath), { recursive: true });
2710
+ await fs6.mkdir(path8.dirname(this.filePath), { recursive: true });
2675
2711
  await this.maybePruneStaleTempFiles();
2676
2712
  } catch (err) {
2677
2713
  if (required) {
@@ -2815,28 +2851,6 @@ function expectDefined(value, label) {
2815
2851
  return value;
2816
2852
  }
2817
2853
 
2818
- // src/utils/session-scoped-path.ts
2819
- import * as path8 from "node:path";
2820
- function sessionScopedPath(dir, sessionId, suffix) {
2821
- if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
2822
- throw invalid(sessionId);
2823
- }
2824
- const resolved = path8.resolve(dir, `${sessionId}${suffix}`);
2825
- const rel = path8.relative(path8.resolve(dir), resolved);
2826
- if (rel.startsWith("..") || path8.isAbsolute(rel)) {
2827
- throw invalid(sessionId);
2828
- }
2829
- return resolved;
2830
- }
2831
- function invalid(sessionId) {
2832
- return new FsError({
2833
- message: `Invalid sessionId: ${sessionId}`,
2834
- code: ERROR_CODES.FS_DELETE_FAILED,
2835
- path: sessionId,
2836
- context: { reason: "path_traversal" }
2837
- });
2838
- }
2839
-
2840
2854
  // src/storage/annotations-store.ts
2841
2855
  var FILE_VERSION = 1;
2842
2856
  var MAX_TEXT_LENGTH = 2e3;
@@ -4628,6 +4642,9 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
4628
4642
  disabledTools: Object.freeze([]),
4629
4643
  autoExtendLimit: true,
4630
4644
  restrictToProjectRoot: true,
4645
+ // Off by default: the board is a record of the work, not a permit for it.
4646
+ // See ToolsConfig.kanbanGovernance for what turning it on costs and gates.
4647
+ kanbanGovernance: false,
4631
4648
  loopDetection: Object.freeze({
4632
4649
  mode: "steer-then-cut",
4633
4650
  steerThreshold: 3,
@@ -4685,6 +4702,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
4685
4702
  disabledTools: DEFAULT_TOOLS_CONFIG.disabledTools,
4686
4703
  autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
4687
4704
  restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot,
4705
+ kanbanGovernance: DEFAULT_TOOLS_CONFIG.kanbanGovernance,
4688
4706
  loopDetection: DEFAULT_TOOLS_CONFIG.loopDetection
4689
4707
  },
4690
4708
  log: { level: "info" },
@@ -5109,6 +5127,14 @@ var IN_PROJECT_DENIED_PATHS = [
5109
5127
  {
5110
5128
  path: "tools.restrictToProjectRoot",
5111
5129
  reason: "The other half of the filesystem confinement switch."
5130
+ },
5131
+ {
5132
+ // Denied for the direction that loosens: the flag defaults to false, so a
5133
+ // repo can only ever use it to turn OFF a gate the user deliberately
5134
+ // enabled. Same class as `tools.restrictToProjectRoot` — a control the
5135
+ // operator owns, not the checked-out repository.
5136
+ path: "tools.kanbanGovernance",
5137
+ reason: "Repo-committed config could disable a Kanban governance gate the operator switched on, letting product mutations run outside any managed card."
5112
5138
  }
5113
5139
  ];
5114
5140
  function deleteNestedPath(target, path34) {
@@ -7682,10 +7708,38 @@ var SessionMemoryConsolidator = class {
7682
7708
  process.stderr.write(`[memory] Session consolidation: ${added} added
7683
7709
  `);
7684
7710
  }
7711
+ if (this.Sage?.rememberSage) {
7712
+ try {
7713
+ const digestBody = buildSessionDigestText(_finalText, _iterations, added);
7714
+ if (digestBody) {
7715
+ const expires = new Date(Date.now() + 14 * 24 * 60 * 6e4).toISOString();
7716
+ await this.Sage.rememberSage({
7717
+ text: digestBody,
7718
+ scope: "session",
7719
+ kind: "session_digest",
7720
+ importance: 0.4,
7721
+ confidence: 0.65,
7722
+ persistence: "short_lived",
7723
+ tags: ["session_digest", "auto"],
7724
+ anchors: [],
7725
+ sources: [{ type: "session", sessionId: _sessionId }],
7726
+ ownerSessionId: _sessionId,
7727
+ expiresAt: expires
7728
+ });
7729
+ }
7730
+ } catch {
7731
+ }
7732
+ }
7685
7733
  } catch {
7686
7734
  }
7687
7735
  };
7688
7736
  };
7737
+ function buildSessionDigestText(finalText, iterations, factsAdded) {
7738
+ const cleaned = finalText.replace(/\s+/g, " ").trim();
7739
+ if (cleaned.length < 40) return void 0;
7740
+ const summary2 = cleaned.slice(0, 400);
7741
+ return `Session digest (${iterations} iter${factsAdded > 0 ? `, ${factsAdded} facts added` : ""}): ` + summary2 + (cleaned.length > 400 ? "\u2026" : "");
7742
+ }
7689
7743
 
7690
7744
  // src/storage/memory-graph-backend.ts
7691
7745
  import * as fs14 from "node:fs/promises";
@@ -9277,11 +9331,15 @@ function sortKeys(value) {
9277
9331
  }
9278
9332
  return value;
9279
9333
  }
9334
+ function semanticMessage(message) {
9335
+ const { ts: _ts, _estTokens: _estimate, origin: _origin, ...semantic } = message;
9336
+ return semantic;
9337
+ }
9280
9338
  function hashRequest(request) {
9281
9339
  const payload = {
9282
9340
  model: request.model,
9283
9341
  system: request.system,
9284
- messages: request.messages,
9342
+ messages: request.messages.map(semanticMessage),
9285
9343
  tools: request.tools,
9286
9344
  maxTokens: request.maxTokens,
9287
9345
  temperature: request.temperature,
@@ -11030,9 +11088,6 @@ import * as path25 from "node:path";
11030
11088
  function shouldSkipSessionDirectoryEntry(name) {
11031
11089
  return name.startsWith(".") && name !== ".wrongstack" || name === "shared" || name === "subagents" || name === "attachments";
11032
11090
  }
11033
- function isSessionJsonlFileName(name) {
11034
- return name.endsWith(".jsonl") && name !== "_index.jsonl";
11035
- }
11036
11091
 
11037
11092
  // src/storage/session-store/directory-session-files.ts
11038
11093
  function sessionIdForFile(prefix, name) {
@@ -11055,7 +11110,7 @@ async function collectSessionFiles(dir, prefix = "", depth = 0) {
11055
11110
  if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
11056
11111
  if (entry.isDirectory()) {
11057
11112
  dirEntries.push(entry);
11058
- } else if (entry.isFile() && isSessionJsonlFileName(entry.name)) {
11113
+ } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
11059
11114
  files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path25.join(dir, entry.name) });
11060
11115
  }
11061
11116
  }
@@ -11079,7 +11134,7 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
11079
11134
  if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
11080
11135
  if (entry.isDirectory()) {
11081
11136
  dirEntries.push(entry);
11082
- } else if (entry.isFile() && isSessionJsonlFileName(entry.name)) {
11137
+ } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
11083
11138
  fileIds.push(sessionIdForFile(prefix, entry.name));
11084
11139
  }
11085
11140
  }
@@ -11243,11 +11298,8 @@ var SessionRecovery = class _SessionRecovery {
11243
11298
  }
11244
11299
  continue;
11245
11300
  }
11246
- if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
11247
- if (entry.name === "_index.jsonl" || entry.name === "_mailbox.jsonl") continue;
11301
+ if (!entry.isFile() || !isSessionTranscriptFileName(entry.name)) continue;
11248
11302
  const base = entry.name.slice(0, -".jsonl".length);
11249
- if (base.includes(".replay") || base.includes(".annotations") || base.includes(".audit"))
11250
- continue;
11251
11303
  const sessionId = prefix ? `${prefix}/${base}` : base;
11252
11304
  const stale = await this.detectStaleExact(sessionId);
11253
11305
  if (stale) out.push(stale);
@@ -12605,6 +12657,15 @@ async function validateResumeFileObservations(events, projectRoot) {
12605
12657
  staleFiles: results.filter((entry) => entry !== null)
12606
12658
  };
12607
12659
  }
12660
+ var RESUME_NOTICE_HEADERS = [
12661
+ "[SESSION RESUME FILE VALIDATION]",
12662
+ "[SESSION RESUME INTERRUPTED WORK]"
12663
+ ];
12664
+ function isResumeNoticeMessage(message) {
12665
+ if (message.role !== "system" || typeof message.content !== "string") return false;
12666
+ return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
12667
+ `));
12668
+ }
12608
12669
  function formatResumeValidationNotice(validation, projectRoot) {
12609
12670
  if (validation.staleFiles.length === 0) return null;
12610
12671
  const root = path28.resolve(projectRoot);
@@ -12801,13 +12862,13 @@ function inheritsIntoFork(event) {
12801
12862
 
12802
12863
  // src/storage/session-store/fork-session.ts
12803
12864
  async function forkSession(host, id, opts = {}) {
12804
- const parent = await host.load(id);
12805
- let boundary = parent.events.length - 1;
12865
+ const parentEvents = await host.readRawEvents(id);
12866
+ let boundary = parentEvents.length - 1;
12806
12867
  let targetCheckpoint;
12807
12868
  if (opts.checkpointPromptIndex !== void 0) {
12808
12869
  boundary = -1;
12809
- for (let i = 0; i < parent.events.length; i++) {
12810
- const event = parent.events[i];
12870
+ for (let i = 0; i < parentEvents.length; i++) {
12871
+ const event = parentEvents[i];
12811
12872
  if (event?.type === "checkpoint" && event.promptIndex === opts.checkpointPromptIndex) {
12812
12873
  boundary = i;
12813
12874
  targetCheckpoint = event;
@@ -12817,15 +12878,18 @@ async function forkSession(host, id, opts = {}) {
12817
12878
  throw new Error(`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`);
12818
12879
  }
12819
12880
  }
12820
- const parentPrefix = parent.events.slice(0, boundary + 1);
12881
+ const parentPrefix = parentEvents.slice(0, boundary + 1);
12821
12882
  const workspaceCheckpoint = targetCheckpoint?.workspaceCheckpoint;
12822
12883
  const checkpointHash = createHash9("sha256").update(parentPrefix.map((event) => JSON.stringify(event)).join("\n") + "\n", "utf8").digest("hex");
12823
12884
  const inherited = parentPrefix.filter(inheritsIntoFork);
12885
+ const start = parentEvents.find(
12886
+ (event) => event.type === "session_start"
12887
+ );
12824
12888
  const writer = await host.create({
12825
12889
  id: "",
12826
- title: parent.metadata.title,
12827
- model: parent.metadata.model,
12828
- provider: parent.metadata.provider
12890
+ title: "",
12891
+ model: start?.model,
12892
+ provider: start?.provider
12829
12893
  });
12830
12894
  try {
12831
12895
  await writer.append({
@@ -12931,6 +12995,17 @@ var SessionLoadCache = class {
12931
12995
  this.entries.clear();
12932
12996
  this.bytes = 0;
12933
12997
  }
12998
+ /**
12999
+ * A hit hands back fresh `messages` / `events` arrays over the cached
13000
+ * contents.
13001
+ *
13002
+ * The entry outlives every caller, and callers treat what they get as their
13003
+ * own: `resume()` passes `messages` straight into a live conversation, and
13004
+ * anything walking `events` may splice it. Returning the cached arrays
13005
+ * themselves let one caller's edit rewrite what the next one loads. The
13006
+ * elements are still shared — copying them would defeat the cache — so
13007
+ * entries remain read-only *contents* behind private containers.
13008
+ */
12934
13009
  getFresh(id, stat15, full) {
12935
13010
  const cached = this.entries.get(id);
12936
13011
  if (!cached || cached.mtimeMs !== stat15.mtimeMs || cached.size !== stat15.size) {
@@ -12938,8 +13013,11 @@ var SessionLoadCache = class {
12938
13013
  }
12939
13014
  this.entries.delete(id);
12940
13015
  this.entries.set(id, cached);
12941
- if (full) return cached.data;
12942
- return { ...cached.data, messages: [] };
13016
+ return {
13017
+ ...cached.data,
13018
+ messages: full ? [...cached.data.messages] : [],
13019
+ events: [...cached.data.events]
13020
+ };
12943
13021
  }
12944
13022
  set(id, stat15, data) {
12945
13023
  this.delete(id);
@@ -13128,6 +13206,9 @@ function stripSnapshotPayload(event) {
13128
13206
  event.messagesOmitted = event.messages.length;
13129
13207
  event.messages = [];
13130
13208
  }
13209
+ function isStrippedSnapshot(event) {
13210
+ return event.messages.length === 0 && typeof event.messagesOmitted === "number" && event.messagesOmitted > 0;
13211
+ }
13131
13212
  function isSessionEventLike(value) {
13132
13213
  return value !== null && typeof value === "object" && typeof value.type === "string" && typeof value.ts === "string";
13133
13214
  }
@@ -13157,7 +13238,12 @@ function replaySessionEvent(params) {
13157
13238
  emitDamaged(params, `Ignored malformed message_updated event at index ${ev.index}`);
13158
13239
  }
13159
13240
  } else if (ev.type === "messages_replaced" && ev.version === 1) {
13160
- if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
13241
+ if (isStrippedSnapshot(ev)) {
13242
+ emitDamaged(
13243
+ params,
13244
+ `Ignored messages_replaced event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
13245
+ );
13246
+ } else if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
13161
13247
  exactJournalActive = true;
13162
13248
  } else {
13163
13249
  emitDamaged(params, "Ignored malformed messages_replaced event");
@@ -13173,9 +13259,16 @@ function replaySessionEvent(params) {
13173
13259
  emitDamaged(params, `Ignored malformed messages_dropped event (count ${String(ev.count)})`);
13174
13260
  }
13175
13261
  } else if (ev.type === "context_snapshot") {
13176
- if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
13262
+ if (isStrippedSnapshot(ev)) {
13263
+ emitDamaged(
13264
+ params,
13265
+ `Ignored context_snapshot event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
13266
+ );
13267
+ } else if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
13177
13268
  emitDamaged(params, "Ignored malformed context_snapshot event");
13178
13269
  }
13270
+ } else if (ev.type === "messages_replaced" || ev.type === "message_appended" || ev.type === "message_updated" || ev.type === "messages_dropped") {
13271
+ emitDamaged(params, `Ignored ${ev.type} event with unsupported version`);
13179
13272
  } else if (!exactJournalActive && ev.type === "user_input") {
13180
13273
  openToolUses.clear();
13181
13274
  messages.push({ role: "user", content: ev.content, ts: ev.ts });
@@ -13228,7 +13321,7 @@ function emitDamaged(params, detail) {
13228
13321
  import * as fsp15 from "node:fs/promises";
13229
13322
  import * as path31 from "node:path";
13230
13323
  function isPrunableSessionJsonl(name) {
13231
- return name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
13324
+ return isSessionTranscriptFileName(name);
13232
13325
  }
13233
13326
  async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13234
13327
  const cutoff = Date.now() - maxAgeDays * 864e5;
@@ -13751,6 +13844,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
13751
13844
  async fork(id, opts = {}) {
13752
13845
  return forkSession(this, id, opts);
13753
13846
  }
13847
+ /**
13848
+ * Implements {@link SessionForkHost.readRawEvents} — the parent stream a fork
13849
+ * inherits, unmodified.
13850
+ *
13851
+ * Deliberately NOT `load()`: that loader empties superseded snapshot payloads
13852
+ * in place and front-drops events past its retention budget, both of which
13853
+ * are correct for reconstructing a conversation and wrong for copying a
13854
+ * journal prefix into a child. Streaming with an accept-everything predicate
13855
+ * keeps the scrubbing contract (`searchEvents` scrubs each line the same way
13856
+ * `load()` does) without either transformation.
13857
+ */
13858
+ async readRawEvents(id) {
13859
+ const hits = await this.searchEvents(id, () => true);
13860
+ return hits.map((hit) => hit.event);
13861
+ }
13754
13862
  /**
13755
13863
  * Capture the deterministic post-tool workspace identity through the store-owned CAS.
13756
13864
  */
@@ -13839,14 +13947,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
13839
13947
  ts: (/* @__PURE__ */ new Date()).toISOString()
13840
13948
  });
13841
13949
  }
13950
+ const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
13842
13951
  const resumedData = {
13843
13952
  ...data,
13844
13953
  ...resumeValidation ? { resumeValidation } : {},
13845
- ...noticeMessages.length > 0 ? { messages: [...data.messages, ...noticeMessages] } : {}
13954
+ messages: [...carriedMessages, ...noticeMessages]
13846
13955
  };
13847
13956
  let handle;
13848
13957
  try {
13849
- handle = await fsp19.open(file, "a", 384);
13958
+ handle = await openSessionForAppend(file);
13850
13959
  } catch (err) {
13851
13960
  emitSessionStoreError(this.events, canonicalId, file, "resume", toErrorMessage(err), false);
13852
13961
  throw new Error(
@@ -14025,10 +14134,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
14025
14134
  const limit = criteria.limit ?? 100;
14026
14135
  if (this.catalogClient) {
14027
14136
  const records = await this.catalogClient.call("list_catalog", {
14028
- limit: Math.min(1e3, Math.max(limit, 100)),
14029
- ...criteria.titleContains ? { search: criteria.titleContains } : {}
14137
+ limit,
14138
+ ...criteria
14030
14139
  });
14031
- return this.scrubSummaries(records).filter((summary2) => matchesSessionFilter(summary2, criteria)).slice(0, limit);
14140
+ return this.scrubSummaries(records);
14032
14141
  }
14033
14142
  try {
14034
14143
  const indexed = await this.readIndex();
@@ -14253,11 +14362,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
14253
14362
  return shardKeys;
14254
14363
  }
14255
14364
  async readOrBuildShardManifest(shardKey) {
14256
- const cached = this.shardManifestCache.get(shardKey);
14257
- if (cached) return cached;
14258
14365
  const manifestPath = this.shardManifestPath(shardKey);
14366
+ const cached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
14367
+ if (cached) return cached;
14259
14368
  return withFileLock(manifestPath, async () => {
14260
- const lockedCached = this.shardManifestCache.get(shardKey);
14369
+ const lockedCached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
14261
14370
  if (lockedCached) return lockedCached;
14262
14371
  const entry = await readOrBuildShardManifestEntry({
14263
14372
  shardKey,
@@ -14268,10 +14377,38 @@ var DefaultSessionStore = class _DefaultSessionStore {
14268
14377
  summaryHeaderFor: (ref) => this.summaryHeaderFor(ref),
14269
14378
  summaryFor: (id) => this.summaryFor(id)
14270
14379
  });
14271
- this.shardManifestCache.set(shardKey, entry);
14380
+ try {
14381
+ const stat15 = await fsp19.stat(manifestPath);
14382
+ this.shardManifestCache.set(shardKey, {
14383
+ entry,
14384
+ mtimeMs: stat15.mtimeMs,
14385
+ size: stat15.size,
14386
+ ino: stat15.ino
14387
+ });
14388
+ } catch {
14389
+ this.shardManifestCache.delete(shardKey);
14390
+ }
14272
14391
  return entry;
14273
14392
  });
14274
14393
  }
14394
+ /**
14395
+ * Shard manifests are invalidated by other store processes via atomic
14396
+ * delete/rebuild. Validate the in-memory projection against the persisted
14397
+ * file so one long-lived process cannot retain another process's stale view.
14398
+ */
14399
+ async freshShardManifestCacheEntry(shardKey, manifestPath) {
14400
+ const cached = this.shardManifestCache.get(shardKey);
14401
+ if (!cached) return void 0;
14402
+ try {
14403
+ const stat15 = await fsp19.stat(manifestPath);
14404
+ if (stat15.mtimeMs === cached.mtimeMs && stat15.size === cached.size && stat15.ino === cached.ino) {
14405
+ return cached.entry;
14406
+ }
14407
+ } catch {
14408
+ }
14409
+ this.shardManifestCache.delete(shardKey);
14410
+ return void 0;
14411
+ }
14275
14412
  async collectSessionFilesInShard(shardKey) {
14276
14413
  const dir = shardKey ? path32.join(this.dir, shardKey) : this.dir;
14277
14414
  const entries = await this.collectSessionFiles(dir, shardKey);
@@ -14551,6 +14688,23 @@ var DefaultSessionStore = class _DefaultSessionStore {
14551
14688
  });
14552
14689
  }
14553
14690
  };
14691
+ async function openSessionForAppend(file) {
14692
+ const handle = await fsp19.open(file, "a+", 384);
14693
+ try {
14694
+ const stat15 = await handle.stat();
14695
+ if (stat15.size > 0) {
14696
+ const tail = Buffer.allocUnsafe(1);
14697
+ const { bytesRead } = await handle.read(tail, 0, 1, stat15.size - 1);
14698
+ if (bytesRead === 1 && tail[0] !== 10) {
14699
+ await handle.appendFile("\n", "utf8");
14700
+ }
14701
+ }
14702
+ return handle;
14703
+ } catch (err) {
14704
+ await handle.close().catch(() => void 0);
14705
+ throw err;
14706
+ }
14707
+ }
14554
14708
 
14555
14709
  // src/storage/session-rewind-apply.ts
14556
14710
  async function applyRewindToConversation(opts) {
@@ -14663,17 +14817,20 @@ var DefaultSessionRewinder = class {
14663
14817
  const targetIndex = checkpoints[n - 1]?.promptIndex ?? 0;
14664
14818
  const snapshotsToRevert = [];
14665
14819
  let shouldRevert = false;
14820
+ let removedEvents = 0;
14666
14821
  for await (const event of this.readEvents(file)) {
14667
14822
  if (event.type === "checkpoint" && event.promptIndex === targetIndex) {
14668
14823
  shouldRevert = true;
14669
14824
  continue;
14670
14825
  }
14671
- if (shouldRevert && event.type === "file_snapshot") {
14826
+ if (!shouldRevert) continue;
14827
+ removedEvents++;
14828
+ if (event.type === "file_snapshot") {
14672
14829
  snapshotsToRevert.push({ promptIndex: event.promptIndex, files: event.files });
14673
14830
  }
14674
14831
  }
14675
14832
  const result = await revertSnapshots(snapshotsToRevert, this.projectRoot);
14676
- return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
14833
+ return { ...result, toPromptIndex: targetIndex, removedEvents };
14677
14834
  }
14678
14835
  async rewindToStart(sessionId) {
14679
14836
  const file = this.sessionFile(sessionId);
@@ -9,7 +9,7 @@ import type { Provider } from '../types/provider.js';
9
9
  * rememberSage rather than the legacy MemoryStore.remember(),
10
10
  * making them visible to searchSage/turn-middleware.
11
11
  */
12
- type ConsolidatorSageKind = 'fact' | 'decision' | 'convention' | 'preference' | 'anti_pattern' | 'warning' | 'workflow' | 'bug_root_cause' | 'file_note' | 'symbol_note' | 'command_note';
12
+ type ConsolidatorSageKind = 'fact' | 'decision' | 'convention' | 'preference' | 'anti_pattern' | 'warning' | 'workflow' | 'bug_root_cause' | 'file_note' | 'symbol_note' | 'command_note' | 'session_digest';
13
13
  interface ConsolidatorMemoryAnchor {
14
14
  type: 'file' | 'directory' | 'symbol' | 'package' | 'command' | 'test' | 'git';
15
15
  path?: string | undefined;
@@ -25,12 +25,14 @@ export interface ConsolidatorSage {
25
25
  priority?: string | undefined;
26
26
  importance?: number | undefined;
27
27
  confidence?: number | undefined;
28
- persistence?: 'long_lived' | undefined;
28
+ persistence?: 'long_lived' | 'short_lived' | undefined;
29
29
  anchors?: ConsolidatorMemoryAnchor[] | undefined;
30
30
  sources?: Array<{
31
31
  type: string;
32
32
  sessionId?: string | undefined;
33
33
  }> | undefined;
34
+ ownerSessionId?: string | undefined;
35
+ expiresAt?: string | undefined;
34
36
  }): Promise<unknown>;
35
37
  listSage?(statuses?: Array<'active'>): Promise<unknown[]>;
36
38
  searchSage?(query: string, opts?: {
@@ -1,6 +1,30 @@
1
1
  import type { ResumeValidation, SessionEvent } from '../types/session.js';
2
2
  /** Revalidate the latest persisted hash for every distinct observed path. */
3
3
  export declare function validateResumeFileObservations(events: readonly SessionEvent[], projectRoot: string): Promise<ResumeValidation>;
4
+ /**
5
+ * Headers of the system messages `resume()` injects.
6
+ *
7
+ * They are described as ephemeral, but every consumer hands the resumed
8
+ * message list to `replaceMessages`, which journals it as a
9
+ * `messages_replaced` snapshot — so a notice written on one resume is replayed
10
+ * as ordinary conversation on the next one, and a fresh notice is added on top.
11
+ * Three resumes with the same modified file left three copies, all but the last
12
+ * describing a check that had already been superseded. `resume()` therefore
13
+ * strips previous notices from the replayed conversation before appending the
14
+ * current ones; these prefixes are how it recognizes them.
15
+ */
16
+ export declare const RESUME_NOTICE_HEADERS: readonly ['[SESSION RESUME FILE VALIDATION]', '[SESSION RESUME INTERRUPTED WORK]'];
17
+ /**
18
+ * True for a system message this module produced on an earlier resume.
19
+ *
20
+ * Deliberately narrow: only a `system` message whose text *starts* with one of
21
+ * the headers matches, so a user or model quoting a notice back is never
22
+ * mistaken for one.
23
+ */
24
+ export declare function isResumeNoticeMessage(message: {
25
+ role: string;
26
+ content: unknown;
27
+ }): boolean;
4
28
  /** Build the ephemeral system message injected into the first resumed turn. */
5
29
  export declare function formatResumeValidationNotice(validation: ResumeValidation, projectRoot: string): string | null;
6
30
  /**
@@ -1,3 +1,7 @@
1
1
  export declare function shouldSkipSessionDirectoryEntry(name: string): boolean;
2
- export declare function isSessionJsonlFileName(name: string): boolean;
2
+ /**
3
+ * Re-exported under the store's own name; the rule itself lives with
4
+ * `sessionScopedPath`, which is what writes the sidecars this excludes.
5
+ */
6
+ export { isSessionTranscriptFileName as isSessionJsonlFileName } from '../../utils/session-scoped-path.js';
3
7
  //# sourceMappingURL=directory-scan.d.ts.map
@@ -1,8 +1,20 @@
1
- import type { ForkedSession, SessionData, SessionForkOptions, SessionMetadata, SessionWriter } from '../../types/session.js';
1
+ import type { ForkedSession, SessionData, SessionEvent, SessionForkOptions, SessionMetadata, SessionWriter } from '../../types/session.js';
2
2
  export interface SessionForkHost {
3
3
  load(id: string): Promise<SessionData>;
4
4
  create(meta: Omit<SessionMetadata, 'startedAt'>): Promise<SessionWriter>;
5
5
  delete(id: string): Promise<void>;
6
+ /**
7
+ * Every persisted event of `id`, exactly as it sits on disk.
8
+ *
9
+ * `load()` is NOT a substitute. Its loader strips the payload out of every
10
+ * superseded `messages_replaced` / `context_snapshot` to bound heap, in
11
+ * place, on the array it returns. Forking from that array copies the
12
+ * emptied snapshots into the child journal, where replay reads them as
13
+ * "the conversation is now zero messages" and the child loses every turn
14
+ * that preceded the newest snapshot inside the fork boundary. A fork is a
15
+ * byte-level inheritance of the parent prefix, so it has to read the bytes.
16
+ */
17
+ readRawEvents(id: string): Promise<SessionEvent[]>;
6
18
  }
7
19
  export declare function forkSession(host: SessionForkHost, id: string, opts?: SessionForkOptions): Promise<ForkedSession>;
8
20
  //# sourceMappingURL=fork-session.d.ts.map
@@ -9,6 +9,17 @@ export declare class SessionLoadCache {
9
9
  constructor(entries?: Map<string, LoadCacheEntry>);
10
10
  private bytes;
11
11
  clear(sessionId?: string): void;
12
+ /**
13
+ * A hit hands back fresh `messages` / `events` arrays over the cached
14
+ * contents.
15
+ *
16
+ * The entry outlives every caller, and callers treat what they get as their
17
+ * own: `resume()` passes `messages` straight into a live conversation, and
18
+ * anything walking `events` may splice it. Returning the cached arrays
19
+ * themselves let one caller's edit rewrite what the next one loads. The
20
+ * elements are still shared — copying them would defeat the cache — so
21
+ * entries remain read-only *contents* behind private containers.
22
+ */
12
23
  getFresh(id: string, stat: FileStatSnapshot, full: boolean): SessionData | null;
13
24
  set(id: string, stat: FileStatSnapshot, data: SessionData): void;
14
25
  private delete;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Prunable === is a transcript. This module held the only complete sidecar
3
+ * list in the codebase while the listing scans held shorter ones; sharing the
4
+ * predicate is what stops them disagreeing again.
5
+ */
1
6
  export declare function isPrunableSessionJsonl(name: string): boolean;
2
7
  export declare function pruneSessionFiles(storeDir: string, maxAgeDays: number, deleteSession: (id: string) => Promise<void>): Promise<number>;
3
8
  //# sourceMappingURL=prune-helpers.d.ts.map