@wrongstack/core 0.306.3 → 0.307.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 (114) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1738 -1690
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/index.js +6103 -5890
  14. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  15. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  16. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  17. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  18. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  19. package/dist/coordination/mailbox-project-server.js +1 -1
  20. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  21. package/dist/coordination/mailbox-types.d.ts +6 -793
  22. package/dist/core/agent-loop-context.d.ts +28 -0
  23. package/dist/core/agent-loop-detector.d.ts +25 -0
  24. package/dist/core/conversation-state.d.ts +1 -2
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/fallback-model.d.ts +19 -1
  27. package/dist/core/fallback-profile-manager.d.ts +21 -3
  28. package/dist/core/index.d.ts +2 -1
  29. package/dist/core/index.js +1143 -766
  30. package/dist/defaults/index.js +3833 -3595
  31. package/dist/execution/auto-compaction-middleware.d.ts +64 -0
  32. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  33. package/dist/execution/autonomy-brain.d.ts +3 -156
  34. package/dist/execution/compaction-budget.d.ts +38 -0
  35. package/dist/execution/compaction-core.d.ts +3 -166
  36. package/dist/execution/compaction-elision.d.ts +58 -0
  37. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  38. package/dist/execution/council-orchestrator.d.ts +3 -42
  39. package/dist/execution/council-response-parser.d.ts +56 -0
  40. package/dist/execution/index.d.ts +1 -1
  41. package/dist/execution/index.js +10016 -9903
  42. package/dist/execution/prompt-enhancer.js +11 -3
  43. package/dist/execution/retry-policy.d.ts +27 -0
  44. package/dist/execution/tool-executor-guard.d.ts +18 -0
  45. package/dist/execution/tool-executor-runner.d.ts +12 -0
  46. package/dist/execution/tool-executor.d.ts +0 -75
  47. package/dist/goal/index.js +19 -6
  48. package/dist/hq/index.js +58 -17
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +8731 -7756
  51. package/dist/infrastructure/index.js +24 -1
  52. package/dist/kernel/events/agent-events.d.ts +2 -0
  53. package/dist/kernel/events/provider-events.d.ts +13 -0
  54. package/dist/plugin/index.js +2123 -1918
  55. package/dist/plugins/auto-review-config.d.ts +53 -0
  56. package/dist/plugins/auto-review-git.d.ts +19 -0
  57. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  58. package/dist/plugins/review-finding-verification.d.ts +7 -0
  59. package/dist/prompts/index.d.ts +1 -0
  60. package/dist/prompts/prompt-journal.d.ts +94 -0
  61. package/dist/security/index.js +677 -667
  62. package/dist/security/permission-explain.d.ts +19 -0
  63. package/dist/security/permission-policy.d.ts +0 -101
  64. package/dist/security/yolo-risk.d.ts +5 -4
  65. package/dist/session-catalog/index.js +270 -227
  66. package/dist/session-catalog/project-server.js +277 -234
  67. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  68. package/dist/session-catalog/store-schema.d.ts +48 -0
  69. package/dist/session-catalog/store.d.ts +0 -17
  70. package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
  71. package/dist/storage/cloud-config-sync.d.ts +1 -1
  72. package/dist/storage/director-state.d.ts +6 -0
  73. package/dist/storage/file-session-writer.d.ts +2 -58
  74. package/dist/storage/index.d.ts +2 -1
  75. package/dist/storage/index.js +2121 -1736
  76. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  77. package/dist/storage/provider-config-watcher.d.ts +9 -0
  78. package/dist/storage/session-recovery.d.ts +9 -0
  79. package/dist/storage/session-store/rename-session.d.ts +16 -0
  80. package/dist/storage/session-store/resume-session.d.ts +26 -0
  81. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  82. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  83. package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
  84. package/dist/storage/session-store.d.ts +2 -129
  85. package/dist/storage/session-summary-tracker.d.ts +39 -0
  86. package/dist/storage/session-write-buffer.d.ts +43 -0
  87. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  88. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  89. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  90. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  91. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  92. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  93. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  94. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  95. package/dist/tools/index.d.ts +1 -1
  96. package/dist/tools/index.js +5150 -5090
  97. package/dist/types/config/root.d.ts +21 -2
  98. package/dist/types/default-config.d.ts +1 -1
  99. package/dist/types/index.d.ts +54 -55
  100. package/dist/types/index.js +953 -932
  101. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  102. package/dist/types/session-markers.d.ts +12 -0
  103. package/dist/types/session-markers.js +19 -0
  104. package/dist/types/session.d.ts +7 -0
  105. package/dist/worktree/worktree-manager.d.ts +1 -1
  106. package/instructions/coordination/subagent-baseline.md +2 -2
  107. package/instructions/llm/prompt-enhancer.md +2 -1
  108. package/instructions/sections/tool/common-patterns.md +21 -0
  109. package/instructions/sections/tool/delegation-compact.md +3 -1
  110. package/instructions/sections/tool/delegation-full.md +5 -1
  111. package/instructions/system-lite.md +28 -3
  112. package/instructions/system-pro.md +37 -8
  113. package/instructions/system.md +4 -2
  114. package/package.json +5 -5
@@ -735,9 +735,9 @@ function hasProjectSessionRegistry(globalRoot) {
735
735
  }
736
736
 
737
737
  // src/session-catalog/store.ts
738
- import { createHash as createHash2, randomBytes, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
739
- import * as fs4 from "node:fs";
740
- import * as path4 from "node:path";
738
+ import { randomBytes, randomUUID as randomUUID2 } from "node:crypto";
739
+ import * as fs5 from "node:fs";
740
+ import * as path5 from "node:path";
741
741
 
742
742
  // src/coordination/sqlite-mailbox-schema.ts
743
743
  import { createRequire } from "node:module";
@@ -777,6 +777,24 @@ function loadDatabaseSync() {
777
777
  }
778
778
 
779
779
  // src/security/secret-scrubber.ts
780
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
781
+ 'Key"',
782
+ 'key"',
783
+ 'KEY"',
784
+ 'token"',
785
+ 'Token"',
786
+ 'TOKEN"',
787
+ 'secret"',
788
+ 'Secret"',
789
+ 'SECRET"',
790
+ 'password"',
791
+ 'Password"',
792
+ 'PASSWORD"',
793
+ 'authorization"',
794
+ 'Authorization"',
795
+ 'bearer"',
796
+ 'Bearer"'
797
+ ];
780
798
  var PATTERNS = [
781
799
  // Anchored at the start where possible so partial matches inside larger
782
800
  // strings don't trigger false positives.
@@ -879,6 +897,30 @@ var PATTERNS = [
879
897
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
880
898
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
881
899
  },
900
+ {
901
+ type: "json_credential_key",
902
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
903
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
904
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
905
+ // "this text may hold a secret", every pattern then declined to match, and
906
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
907
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
908
+ // never matched.
909
+ //
910
+ // Tool results are routinely serialised as JSON, and a credential with no
911
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
912
+ // has no other pattern that can catch it — this is the only thing standing
913
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
914
+ // the model's own context.
915
+ //
916
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
917
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
918
+ // the closing quote has to follow the word immediately.
919
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
920
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
921
+ regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
922
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
923
+ },
882
924
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
883
925
  // The plugin runtime carried 37 patterns while this scrubber — the one that
884
926
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -961,9 +1003,12 @@ var PATTERNS = [
961
1003
  anchor: "GOCSPX-"
962
1004
  }
963
1005
  ];
964
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
1006
+ var SIMPLE_PATTERNS = PATTERNS.filter(
1007
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
1008
+ );
965
1009
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
966
1010
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
1011
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
967
1012
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
968
1013
  var SCRUB_CHUNK_BYTES = 64 * 1024;
969
1014
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -974,20 +1019,7 @@ var PATTERN_ANCHORS = [
974
1019
  )
975
1020
  )
976
1021
  ];
977
- var JSON_KEY_ANCHORS = [
978
- '"apiKey"',
979
- '"api_key"',
980
- '"token"',
981
- '"secret"',
982
- '"password"',
983
- '"authorization"',
984
- '"bearer"',
985
- '"private_key"',
986
- '"access_token"',
987
- '"refresh_token"',
988
- '"client_secret"'
989
- ];
990
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
1022
+ var ALL_ANCHORS = PATTERN_ANCHORS;
991
1023
  function hasCredentialAnchors(text) {
992
1024
  for (const anchor of ALL_ANCHORS) {
993
1025
  if (text.includes(anchor)) return true;
@@ -1036,6 +1068,9 @@ var DefaultSecretScrubber = class {
1036
1068
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
1037
1069
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
1038
1070
  });
1071
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
1072
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
1073
+ });
1039
1074
  return out;
1040
1075
  }
1041
1076
  /**
@@ -1198,6 +1233,10 @@ function isPidAlive(pid) {
1198
1233
  }
1199
1234
  }
1200
1235
 
1236
+ // src/session-catalog/store-rebuild.ts
1237
+ import * as fs4 from "node:fs";
1238
+ import * as path4 from "node:path";
1239
+
1201
1240
  // src/utils/session-scoped-path.ts
1202
1241
  var SESSION_SIDECAR_JSONL_SUFFIXES = [
1203
1242
  ".replay.jsonl",
@@ -1214,7 +1253,8 @@ function isSessionTranscriptFileName(name) {
1214
1253
  return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
1215
1254
  }
1216
1255
 
1217
- // src/session-catalog/store.ts
1256
+ // src/session-catalog/store-schema.ts
1257
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
1218
1258
  var SCHEMA_VERSION = 1;
1219
1259
  var MAX_LEASE_MS = 12e4;
1220
1260
  var MAX_RESERVATION_MS = 6e4;
@@ -1259,12 +1299,192 @@ function boundPresenceValue(value, depth) {
1259
1299
  }
1260
1300
  return result;
1261
1301
  }
1302
+ function configureCatalogDatabase(db) {
1303
+ db.exec(
1304
+ "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;"
1305
+ );
1306
+ }
1307
+ function initializeCatalogSchema(db) {
1308
+ db.exec(`
1309
+ CREATE TABLE IF NOT EXISTS catalog_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
1310
+ CREATE TABLE IF NOT EXISTS sessions (
1311
+ session_id TEXT PRIMARY KEY,
1312
+ transcript_relative_path TEXT NOT NULL,
1313
+ summary_relative_path TEXT NOT NULL,
1314
+ summary_json TEXT NOT NULL,
1315
+ transcript_size INTEGER NOT NULL DEFAULT 0,
1316
+ transcript_mtime_ms REAL NOT NULL DEFAULT 0,
1317
+ summary_revision INTEGER NOT NULL DEFAULT 1,
1318
+ indexed_at TEXT NOT NULL,
1319
+ damaged INTEGER NOT NULL DEFAULT 0
1320
+ );
1321
+ CREATE TABLE IF NOT EXISTS session_leases (
1322
+ session_id TEXT PRIMARY KEY,
1323
+ lease_id TEXT NOT NULL UNIQUE,
1324
+ lease_secret_hash TEXT NOT NULL,
1325
+ owner_instance_id TEXT NOT NULL,
1326
+ owner_pid INTEGER NOT NULL,
1327
+ owner_started_at TEXT NOT NULL,
1328
+ entry_json TEXT NOT NULL,
1329
+ agent_revision INTEGER NOT NULL DEFAULT 0,
1330
+ status TEXT NOT NULL,
1331
+ last_heartbeat_at INTEGER NOT NULL,
1332
+ lease_expires_at INTEGER NOT NULL
1333
+ );
1334
+ CREATE TABLE IF NOT EXISTS resume_reservations (
1335
+ reservation_id TEXT PRIMARY KEY,
1336
+ target_session_id TEXT NOT NULL UNIQUE,
1337
+ requester_instance_id TEXT NOT NULL,
1338
+ current_session_id TEXT,
1339
+ created_at INTEGER NOT NULL,
1340
+ expires_at INTEGER NOT NULL
1341
+ );
1342
+ CREATE TABLE IF NOT EXISTS maintenance_leases (
1343
+ session_id TEXT PRIMARY KEY,
1344
+ operation TEXT NOT NULL,
1345
+ holder_id TEXT NOT NULL,
1346
+ lease_id TEXT NOT NULL UNIQUE,
1347
+ acquired_at INTEGER NOT NULL,
1348
+ expires_at INTEGER NOT NULL
1349
+ );
1350
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(indexed_at DESC);
1351
+ CREATE INDEX IF NOT EXISTS idx_leases_expiry ON session_leases(lease_expires_at);
1352
+ CREATE INDEX IF NOT EXISTS idx_reservations_expiry ON resume_reservations(expires_at);
1353
+ CREATE INDEX IF NOT EXISTS idx_maintenance_expiry ON maintenance_leases(expires_at);
1354
+ `);
1355
+ db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("schema_version", String(SCHEMA_VERSION));
1356
+ db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("generation", "0");
1357
+ const row = db.prepare("SELECT value FROM catalog_meta WHERE key=?").get("schema_version");
1358
+ if (Number(row.value) !== SCHEMA_VERSION)
1359
+ throw new Error(`Unsupported session catalog schema ${row.value}`);
1360
+ }
1361
+
1362
+ // src/session-catalog/store-rebuild.ts
1363
+ function walkSessionFiles(root, suffix) {
1364
+ const result = [];
1365
+ const visit = (dir) => {
1366
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1367
+ if (entry.isDirectory()) {
1368
+ if (entry.name !== "_cas" && entry.name !== "_trash") visit(path4.join(dir, entry.name));
1369
+ } else if (entry.isFile() && entry.name.endsWith(suffix))
1370
+ result.push(path4.join(dir, entry.name));
1371
+ }
1372
+ };
1373
+ visit(root);
1374
+ return result;
1375
+ }
1376
+ function summarizeTranscriptFile(transcriptFilePath, id) {
1377
+ const lines = fs4.readFileSync(transcriptFilePath, "utf8").split(/\r?\n/);
1378
+ let start;
1379
+ let endedAt;
1380
+ let lastActivityAt;
1381
+ let messageCount = 0;
1382
+ let iterationCount = 0;
1383
+ let toolCallCount = 0;
1384
+ let compactionCount = 0;
1385
+ let tokenTotal = 0;
1386
+ for (const line of lines) {
1387
+ if (!line) continue;
1388
+ let event;
1389
+ try {
1390
+ event = parseJson(line);
1391
+ } catch {
1392
+ continue;
1393
+ }
1394
+ lastActivityAt = event.ts;
1395
+ if (event.type === "session_start") start = event;
1396
+ if (event.type === "session_end") endedAt = event.ts;
1397
+ if (event.type === "message_appended" && (event.message.role === "user" || event.message.role === "assistant"))
1398
+ messageCount++;
1399
+ if (event.type === "llm_response") {
1400
+ iterationCount++;
1401
+ tokenTotal += event.usage.input + event.usage.output + (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
1402
+ }
1403
+ if (event.type === "tool_call_end") toolCallCount++;
1404
+ if (event.type === "compaction") compactionCount++;
1405
+ }
1406
+ if (!start) throw new Error("missing session_start");
1407
+ return {
1408
+ id,
1409
+ title: id,
1410
+ startedAt: start.ts,
1411
+ ...endedAt ? { endedAt } : {},
1412
+ model: start.model,
1413
+ provider: start.provider,
1414
+ tokenTotal,
1415
+ ...lastActivityAt ? { lastActivityAt } : {},
1416
+ messageCount,
1417
+ iterationCount,
1418
+ toolCallCount,
1419
+ compactionCount
1420
+ };
1421
+ }
1422
+ function rebuildCatalogIndex(db, sessionsDir, containedPathFn, transactionFn, bumpGenerationFn) {
1423
+ const summaries = walkSessionFiles(sessionsDir, ".summary.json");
1424
+ const transcripts = walkSessionFiles(sessionsDir, ".jsonl").filter(
1425
+ (file) => isSessionTranscriptFileName(path4.basename(file))
1426
+ );
1427
+ const ids = /* @__PURE__ */ new Set();
1428
+ for (const file of [...summaries, ...transcripts]) {
1429
+ const relative2 = path4.relative(sessionsDir, file).replaceAll("\\", "/");
1430
+ ids.add(relative2.replace(/\.summary\.json$|\.jsonl$/, ""));
1431
+ }
1432
+ let indexed = 0;
1433
+ let damaged = 0;
1434
+ transactionFn(() => {
1435
+ db.prepare("DELETE FROM sessions").run();
1436
+ for (const id of ids) {
1437
+ try {
1438
+ const summaryFile = containedPathFn(`${id}.summary.json`);
1439
+ const summary = fs4.existsSync(summaryFile) ? parseJson(fs4.readFileSync(summaryFile, "utf8")) : summarizeTranscriptFile(containedPathFn(`${id}.jsonl`), id);
1440
+ if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
1441
+ const transcript = containedPathFn(`${id}.jsonl`);
1442
+ const stat = fs4.existsSync(transcript) ? fs4.statSync(transcript) : void 0;
1443
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1444
+ db.prepare(
1445
+ "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,transcript_size,transcript_mtime_ms,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,?,?,?,?,0)"
1446
+ ).run(
1447
+ id,
1448
+ `${id}.jsonl`,
1449
+ `${id}.summary.json`,
1450
+ JSON.stringify(summary),
1451
+ stat?.size ?? 0,
1452
+ stat?.mtimeMs ?? 0,
1453
+ 1,
1454
+ now
1455
+ );
1456
+ indexed++;
1457
+ } catch {
1458
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1459
+ const fallback = {
1460
+ id,
1461
+ title: id,
1462
+ startedAt: now,
1463
+ model: "",
1464
+ provider: "",
1465
+ tokenTotal: 0
1466
+ };
1467
+ db.prepare(
1468
+ "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,1,?,1)"
1469
+ ).run(id, `${id}.jsonl`, `${id}.summary.json`, JSON.stringify(fallback), now);
1470
+ damaged++;
1471
+ }
1472
+ }
1473
+ db.prepare(
1474
+ "INSERT INTO catalog_meta(key,value) VALUES ('last_reconciliation',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
1475
+ ).run((/* @__PURE__ */ new Date()).toISOString());
1476
+ bumpGenerationFn();
1477
+ });
1478
+ return { indexed, damaged };
1479
+ }
1480
+
1481
+ // src/session-catalog/store.ts
1262
1482
  var SessionCatalogStore = class {
1263
1483
  constructor(projectDir) {
1264
1484
  this.projectDir = projectDir;
1265
- this.sessionsDir = path4.join(projectDir, "sessions");
1266
- fs4.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
1267
- this.databasePath = path4.join(this.sessionsDir, "catalog.sqlite");
1485
+ this.sessionsDir = path5.join(projectDir, "sessions");
1486
+ fs5.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
1487
+ this.databasePath = path5.join(this.sessionsDir, "catalog.sqlite");
1268
1488
  const Database = loadDatabaseSync();
1269
1489
  this.db = new Database(this.databasePath);
1270
1490
  try {
@@ -1281,12 +1501,12 @@ var SessionCatalogStore = class {
1281
1501
  this.db.close();
1282
1502
  const quarantine = `${this.databasePath}.corrupt-${Date.now()}`;
1283
1503
  try {
1284
- fs4.renameSync(this.databasePath, quarantine);
1504
+ fs5.renameSync(this.databasePath, quarantine);
1285
1505
  } catch {
1286
1506
  }
1287
1507
  for (const suffix of ["-wal", "-shm"]) {
1288
1508
  try {
1289
- fs4.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
1509
+ fs5.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
1290
1510
  } catch {
1291
1511
  }
1292
1512
  }
@@ -1298,7 +1518,7 @@ var SessionCatalogStore = class {
1298
1518
  const rowCount = Number(
1299
1519
  this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get().count
1300
1520
  );
1301
- if (rowCount === 0 && this.walkFiles(this.sessionsDir, ".jsonl").some((file) => !file.endsWith("_index.jsonl"))) {
1521
+ if (rowCount === 0 && walkSessionFiles(this.sessionsDir, ".jsonl").some((file) => !file.endsWith("_index.jsonl"))) {
1302
1522
  this.rebuildCatalog();
1303
1523
  }
1304
1524
  }
@@ -1308,66 +1528,13 @@ var SessionCatalogStore = class {
1308
1528
  db;
1309
1529
  scrubber = new DefaultSecretScrubber();
1310
1530
  configureDatabase() {
1311
- this.db.exec(
1312
- "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;"
1313
- );
1531
+ configureCatalogDatabase(this.db);
1314
1532
  }
1315
1533
  close() {
1316
1534
  this.db.close();
1317
1535
  }
1318
1536
  initialize() {
1319
- this.db.exec(`
1320
- CREATE TABLE IF NOT EXISTS catalog_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
1321
- CREATE TABLE IF NOT EXISTS sessions (
1322
- session_id TEXT PRIMARY KEY,
1323
- transcript_relative_path TEXT NOT NULL,
1324
- summary_relative_path TEXT NOT NULL,
1325
- summary_json TEXT NOT NULL,
1326
- transcript_size INTEGER NOT NULL DEFAULT 0,
1327
- transcript_mtime_ms REAL NOT NULL DEFAULT 0,
1328
- summary_revision INTEGER NOT NULL DEFAULT 1,
1329
- indexed_at TEXT NOT NULL,
1330
- damaged INTEGER NOT NULL DEFAULT 0
1331
- );
1332
- CREATE TABLE IF NOT EXISTS session_leases (
1333
- session_id TEXT PRIMARY KEY,
1334
- lease_id TEXT NOT NULL UNIQUE,
1335
- lease_secret_hash TEXT NOT NULL,
1336
- owner_instance_id TEXT NOT NULL,
1337
- owner_pid INTEGER NOT NULL,
1338
- owner_started_at TEXT NOT NULL,
1339
- entry_json TEXT NOT NULL,
1340
- agent_revision INTEGER NOT NULL DEFAULT 0,
1341
- status TEXT NOT NULL,
1342
- last_heartbeat_at INTEGER NOT NULL,
1343
- lease_expires_at INTEGER NOT NULL
1344
- );
1345
- CREATE TABLE IF NOT EXISTS resume_reservations (
1346
- reservation_id TEXT PRIMARY KEY,
1347
- target_session_id TEXT NOT NULL UNIQUE,
1348
- requester_instance_id TEXT NOT NULL,
1349
- current_session_id TEXT,
1350
- created_at INTEGER NOT NULL,
1351
- expires_at INTEGER NOT NULL
1352
- );
1353
- CREATE TABLE IF NOT EXISTS maintenance_leases (
1354
- session_id TEXT PRIMARY KEY,
1355
- operation TEXT NOT NULL,
1356
- holder_id TEXT NOT NULL,
1357
- lease_id TEXT NOT NULL UNIQUE,
1358
- acquired_at INTEGER NOT NULL,
1359
- expires_at INTEGER NOT NULL
1360
- );
1361
- CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(indexed_at DESC);
1362
- CREATE INDEX IF NOT EXISTS idx_leases_expiry ON session_leases(lease_expires_at);
1363
- CREATE INDEX IF NOT EXISTS idx_reservations_expiry ON resume_reservations(expires_at);
1364
- CREATE INDEX IF NOT EXISTS idx_maintenance_expiry ON maintenance_leases(expires_at);
1365
- `);
1366
- this.db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("schema_version", String(SCHEMA_VERSION));
1367
- this.db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("generation", "0");
1368
- const row = this.db.prepare("SELECT value FROM catalog_meta WHERE key=?").get("schema_version");
1369
- if (Number(row.value) !== SCHEMA_VERSION)
1370
- throw new Error(`Unsupported session catalog schema ${row.value}`);
1537
+ initializeCatalogSchema(this.db);
1371
1538
  }
1372
1539
  transaction(run) {
1373
1540
  this.db.exec("BEGIN IMMEDIATE");
@@ -1408,21 +1575,6 @@ var SessionCatalogStore = class {
1408
1575
  leaseRow(sessionId) {
1409
1576
  return this.db.prepare("SELECT * FROM session_leases WHERE session_id=?").get(sessionId);
1410
1577
  }
1411
- /**
1412
- * True when `sessionId` has a live `session_leases` row owned by a
1413
- * different OS process than the *caller*. Used by
1414
- * `acquireMaintenance` to distinguish "another running wstack" (which
1415
- * must be fenced off) from "this very TUI owning its own session"
1416
- * (which is allowed to claim a non-destructive maintenance lease on
1417
- * its own transcript).
1418
- *
1419
- * `callerPid` must be the pid of the process that asked for the lease,
1420
- * NOT `process.pid`. In the built runtime this store lives inside the
1421
- * detached project-catalog daemon, so `process.pid` is the daemon's and
1422
- * would mark every lease — including the caller's own session — foreign,
1423
- * making `/clear` and `/rewind` fail with "Session … is live". Callers
1424
- * that run the store in-process may omit it.
1425
- */
1426
1578
  foreignLiveLease(sessionId, callerPid = process.pid) {
1427
1579
  const live = this.leaseRow(sessionId);
1428
1580
  return Boolean(live) && live.owner_pid !== callerPid;
@@ -1512,7 +1664,7 @@ var SessionCatalogStore = class {
1512
1664
  if (this.maintenanceExists(targetSessionId))
1513
1665
  throw conflict(`Session ${targetSessionId} is under maintenance`);
1514
1666
  const catalog = this.db.prepare("SELECT 1 AS yes FROM sessions WHERE session_id=?").get(targetSessionId);
1515
- if (!catalog && !fs4.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
1667
+ if (!catalog && !fs5.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
1516
1668
  throw new Error(`Session not found: ${targetSessionId}`);
1517
1669
  const reservationId = randomUUID2();
1518
1670
  const now = Date.now();
@@ -1640,9 +1792,9 @@ var SessionCatalogStore = class {
1640
1792
  }
1641
1793
  containedPath(relative2) {
1642
1794
  assertId(relative2.replace(/\.(jsonl|summary\.json)$/, ""), "session path");
1643
- const root = path4.resolve(this.sessionsDir);
1644
- const candidate = path4.resolve(root, relative2);
1645
- const prefix = `${root}${path4.sep}`;
1795
+ const root = path5.resolve(this.sessionsDir);
1796
+ const candidate = path5.resolve(root, relative2);
1797
+ const prefix = `${root}${path5.sep}`;
1646
1798
  if (candidate !== root && !(process.platform === "win32" ? candidate.toLowerCase().startsWith(prefix.toLowerCase()) : candidate.startsWith(prefix)))
1647
1799
  throw new TypeError("Session path escapes sessions directory");
1648
1800
  return candidate;
@@ -1658,7 +1810,7 @@ var SessionCatalogStore = class {
1658
1810
  transcriptRelativePath = normalizedTranscript;
1659
1811
  summaryRelativePath = normalizedSummary;
1660
1812
  const transcript = this.containedPath(transcriptRelativePath);
1661
- const stat = fs4.existsSync(transcript) ? fs4.statSync(transcript) : void 0;
1813
+ const stat = fs5.existsSync(transcript) ? fs5.statSync(transcript) : void 0;
1662
1814
  const now = (/* @__PURE__ */ new Date()).toISOString();
1663
1815
  return this.transaction(() => {
1664
1816
  const prior = this.db.prepare("SELECT summary_revision FROM sessions WHERE session_id=?").get(summary.id);
@@ -1781,7 +1933,7 @@ var SessionCatalogStore = class {
1781
1933
  if (trimmed) summary.name = this.scrubber.scrub(trimmed).slice(0, 500);
1782
1934
  else delete summary.name;
1783
1935
  const summaryPath = this.containedPath(current.summaryRelativePath);
1784
- fs4.mkdirSync(path4.dirname(summaryPath), { recursive: true, mode: 448 });
1936
+ fs5.mkdirSync(path5.dirname(summaryPath), { recursive: true, mode: 448 });
1785
1937
  await atomicWrite(summaryPath, `${JSON.stringify(summary)}
1786
1938
  `, { mode: 384 });
1787
1939
  try {
@@ -1841,16 +1993,16 @@ var SessionCatalogStore = class {
1841
1993
  this.containedPath(`${sessionId}.plan.json`),
1842
1994
  this.containedPath(`${sessionId}.tasks.json`),
1843
1995
  this.containedPath(`${sessionId}.todos.json`),
1844
- path4.join(path4.dirname(transcript), path4.basename(sessionId))
1996
+ path5.join(path5.dirname(transcript), path5.basename(sessionId))
1845
1997
  ];
1846
- const trashRoot = path4.join(this.sessionsDir, "_trash", lease.leaseId);
1847
- fs4.mkdirSync(trashRoot, { recursive: true, mode: 448 });
1998
+ const trashRoot = path5.join(this.sessionsDir, "_trash", lease.leaseId);
1999
+ fs5.mkdirSync(trashRoot, { recursive: true, mode: 448 });
1848
2000
  const moved = [];
1849
2001
  try {
1850
2002
  artifacts.forEach((artifact, index) => {
1851
- if (!fs4.existsSync(artifact)) return;
1852
- const target = path4.join(trashRoot, `${index}-${path4.basename(artifact)}`);
1853
- fs4.renameSync(artifact, target);
2003
+ if (!fs5.existsSync(artifact)) return;
2004
+ const target = path5.join(trashRoot, `${index}-${path5.basename(artifact)}`);
2005
+ fs5.renameSync(artifact, target);
1854
2006
  moved.push({ from: artifact, to: target });
1855
2007
  });
1856
2008
  this.transaction(() => {
@@ -1865,17 +2017,17 @@ var SessionCatalogStore = class {
1865
2017
  } catch (error) {
1866
2018
  for (const item of moved.reverse()) {
1867
2019
  try {
1868
- fs4.mkdirSync(path4.dirname(item.from), { recursive: true, mode: 448 });
1869
- fs4.renameSync(item.to, item.from);
2020
+ fs5.mkdirSync(path5.dirname(item.from), { recursive: true, mode: 448 });
2021
+ fs5.renameSync(item.to, item.from);
1870
2022
  } catch {
1871
2023
  }
1872
2024
  }
1873
2025
  throw error;
1874
2026
  }
1875
2027
  try {
1876
- fs4.rmSync(trashRoot, { recursive: true, force: true });
1877
- const trashParent = path4.dirname(trashRoot);
1878
- if (fs4.readdirSync(trashParent).length === 0) fs4.rmdirSync(trashParent);
2028
+ fs5.rmSync(trashRoot, { recursive: true, force: true });
2029
+ const trashParent = path5.dirname(trashRoot);
2030
+ if (fs5.readdirSync(trashParent).length === 0) fs5.rmdirSync(trashParent);
1879
2031
  } catch {
1880
2032
  }
1881
2033
  }
@@ -1896,122 +2048,13 @@ var SessionCatalogStore = class {
1896
2048
  return deleted;
1897
2049
  }
1898
2050
  rebuildCatalog() {
1899
- const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
1900
- const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
1901
- (file) => isSessionTranscriptFileName(path4.basename(file))
2051
+ return rebuildCatalogIndex(
2052
+ this.db,
2053
+ this.sessionsDir,
2054
+ (rel) => this.containedPath(rel),
2055
+ (run) => this.transaction(run),
2056
+ () => this.bumpGeneration()
1902
2057
  );
1903
- const ids = /* @__PURE__ */ new Set();
1904
- for (const file of [...summaries, ...transcripts]) {
1905
- const relative2 = path4.relative(this.sessionsDir, file).replaceAll("\\", "/");
1906
- ids.add(relative2.replace(/\.summary\.json$|\.jsonl$/, ""));
1907
- }
1908
- let indexed = 0;
1909
- let damaged = 0;
1910
- this.transaction(() => {
1911
- this.db.prepare("DELETE FROM sessions").run();
1912
- for (const id of ids) {
1913
- try {
1914
- const summaryFile = this.containedPath(`${id}.summary.json`);
1915
- const summary = fs4.existsSync(summaryFile) ? parseJson(fs4.readFileSync(summaryFile, "utf8")) : this.summarizeTranscript(id);
1916
- if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
1917
- const transcript = this.containedPath(`${id}.jsonl`);
1918
- const stat = fs4.existsSync(transcript) ? fs4.statSync(transcript) : void 0;
1919
- const now = (/* @__PURE__ */ new Date()).toISOString();
1920
- this.db.prepare(
1921
- "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,transcript_size,transcript_mtime_ms,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,?,?,?,?,0)"
1922
- ).run(
1923
- id,
1924
- `${id}.jsonl`,
1925
- `${id}.summary.json`,
1926
- JSON.stringify(summary),
1927
- stat?.size ?? 0,
1928
- stat?.mtimeMs ?? 0,
1929
- 1,
1930
- now
1931
- );
1932
- indexed++;
1933
- } catch {
1934
- const now = (/* @__PURE__ */ new Date()).toISOString();
1935
- const fallback = {
1936
- id,
1937
- title: id,
1938
- startedAt: now,
1939
- model: "",
1940
- provider: "",
1941
- tokenTotal: 0
1942
- };
1943
- this.db.prepare(
1944
- "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,1,?,1)"
1945
- ).run(id, `${id}.jsonl`, `${id}.summary.json`, JSON.stringify(fallback), now);
1946
- damaged++;
1947
- }
1948
- }
1949
- this.db.prepare(
1950
- "INSERT INTO catalog_meta(key,value) VALUES ('last_reconciliation',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
1951
- ).run((/* @__PURE__ */ new Date()).toISOString());
1952
- this.bumpGeneration();
1953
- });
1954
- return { indexed, damaged };
1955
- }
1956
- walkFiles(root, suffix) {
1957
- const result = [];
1958
- const visit = (dir) => {
1959
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1960
- if (entry.isDirectory()) {
1961
- if (entry.name !== "_cas" && entry.name !== "_trash") visit(path4.join(dir, entry.name));
1962
- } else if (entry.isFile() && entry.name.endsWith(suffix))
1963
- result.push(path4.join(dir, entry.name));
1964
- }
1965
- };
1966
- visit(root);
1967
- return result;
1968
- }
1969
- summarizeTranscript(id) {
1970
- const file = this.containedPath(`${id}.jsonl`);
1971
- const lines = fs4.readFileSync(file, "utf8").split(/\r?\n/);
1972
- let start;
1973
- let endedAt;
1974
- let lastActivityAt;
1975
- let messageCount = 0;
1976
- let iterationCount = 0;
1977
- let toolCallCount = 0;
1978
- let compactionCount = 0;
1979
- let tokenTotal = 0;
1980
- for (const line of lines) {
1981
- if (!line) continue;
1982
- let event;
1983
- try {
1984
- event = parseJson(line);
1985
- } catch {
1986
- continue;
1987
- }
1988
- lastActivityAt = event.ts;
1989
- if (event.type === "session_start") start = event;
1990
- if (event.type === "session_end") endedAt = event.ts;
1991
- if (event.type === "message_appended" && (event.message.role === "user" || event.message.role === "assistant"))
1992
- messageCount++;
1993
- if (event.type === "llm_response") {
1994
- iterationCount++;
1995
- tokenTotal += event.usage.input + event.usage.output + (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
1996
- }
1997
- if (event.type === "tool_call_end") toolCallCount++;
1998
- if (event.type === "compaction") compactionCount++;
1999
- }
2000
- if (!start) throw new Error("missing session_start");
2001
- return {
2002
- id,
2003
- title: id,
2004
- startedAt: start.ts,
2005
- ...endedAt ? { endedAt } : {},
2006
- model: start.model,
2007
- provider: start.provider,
2008
- tokenTotal,
2009
- ...lastActivityAt ? { lastActivityAt } : {},
2010
- messageCount,
2011
- iterationCount,
2012
- toolCallCount,
2013
- compactionCount
2014
- };
2015
2058
  }
2016
2059
  health(base) {
2017
2060
  this.reapExpired();