@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
@@ -4,7 +4,7 @@
4
4
  import { randomBytes as randomBytes2, randomUUID as randomUUID2 } from "node:crypto";
5
5
  import * as fsp from "node:fs/promises";
6
6
  import * as net from "node:net";
7
- import * as path3 from "node:path";
7
+ import * as path4 from "node:path";
8
8
  import { bindProjectEndpoint } from "@wrongstack/persistence";
9
9
 
10
10
  // src/security/file-permissions.ts
@@ -179,9 +179,9 @@ function sessionCatalogProjectServerMetadataPath(projectDir) {
179
179
  }
180
180
 
181
181
  // src/session-catalog/store.ts
182
- import { createHash as createHash2, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
183
- import * as fs from "node:fs";
184
- import * as path2 from "node:path";
182
+ import { randomBytes, randomUUID } from "node:crypto";
183
+ import * as fs2 from "node:fs";
184
+ import * as path3 from "node:path";
185
185
 
186
186
  // src/coordination/sqlite-mailbox-schema.ts
187
187
  import { createRequire } from "node:module";
@@ -221,6 +221,24 @@ function loadDatabaseSync() {
221
221
  }
222
222
 
223
223
  // src/security/secret-scrubber.ts
224
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
225
+ 'Key"',
226
+ 'key"',
227
+ 'KEY"',
228
+ 'token"',
229
+ 'Token"',
230
+ 'TOKEN"',
231
+ 'secret"',
232
+ 'Secret"',
233
+ 'SECRET"',
234
+ 'password"',
235
+ 'Password"',
236
+ 'PASSWORD"',
237
+ 'authorization"',
238
+ 'Authorization"',
239
+ 'bearer"',
240
+ 'Bearer"'
241
+ ];
224
242
  var PATTERNS = [
225
243
  // Anchored at the start where possible so partial matches inside larger
226
244
  // strings don't trigger false positives.
@@ -323,6 +341,30 @@ var PATTERNS = [
323
341
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
324
342
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
325
343
  },
344
+ {
345
+ type: "json_credential_key",
346
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
347
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
348
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
349
+ // "this text may hold a secret", every pattern then declined to match, and
350
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
351
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
352
+ // never matched.
353
+ //
354
+ // Tool results are routinely serialised as JSON, and a credential with no
355
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
356
+ // has no other pattern that can catch it — this is the only thing standing
357
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
358
+ // the model's own context.
359
+ //
360
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
361
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
362
+ // the closing quote has to follow the word immediately.
363
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
364
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
365
+ 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,
366
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
367
+ },
326
368
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
327
369
  // The plugin runtime carried 37 patterns while this scrubber — the one that
328
370
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -405,9 +447,12 @@ var PATTERNS = [
405
447
  anchor: "GOCSPX-"
406
448
  }
407
449
  ];
408
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
450
+ var SIMPLE_PATTERNS = PATTERNS.filter(
451
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
452
+ );
409
453
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
410
454
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
455
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
411
456
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
412
457
  var SCRUB_CHUNK_BYTES = 64 * 1024;
413
458
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -418,20 +463,7 @@ var PATTERN_ANCHORS = [
418
463
  )
419
464
  )
420
465
  ];
421
- var JSON_KEY_ANCHORS = [
422
- '"apiKey"',
423
- '"api_key"',
424
- '"token"',
425
- '"secret"',
426
- '"password"',
427
- '"authorization"',
428
- '"bearer"',
429
- '"private_key"',
430
- '"access_token"',
431
- '"refresh_token"',
432
- '"client_secret"'
433
- ];
434
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
466
+ var ALL_ANCHORS = PATTERN_ANCHORS;
435
467
  function hasCredentialAnchors(text) {
436
468
  for (const anchor of ALL_ANCHORS) {
437
469
  if (text.includes(anchor)) return true;
@@ -480,6 +512,9 @@ var DefaultSecretScrubber = class {
480
512
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
481
513
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
482
514
  });
515
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
516
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
517
+ });
483
518
  return out;
484
519
  }
485
520
  /**
@@ -521,6 +556,10 @@ function isPidAlive(pid) {
521
556
  }
522
557
  }
523
558
 
559
+ // src/session-catalog/store-rebuild.ts
560
+ import * as fs from "node:fs";
561
+ import * as path2 from "node:path";
562
+
524
563
  // src/utils/session-scoped-path.ts
525
564
  var SESSION_SIDECAR_JSONL_SUFFIXES = [
526
565
  ".replay.jsonl",
@@ -537,7 +576,8 @@ function isSessionTranscriptFileName(name) {
537
576
  return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
538
577
  }
539
578
 
540
- // src/session-catalog/store.ts
579
+ // src/session-catalog/store-schema.ts
580
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
541
581
  var SCHEMA_VERSION = 1;
542
582
  var MAX_LEASE_MS = 12e4;
543
583
  var MAX_RESERVATION_MS = 6e4;
@@ -582,12 +622,192 @@ function boundPresenceValue(value, depth) {
582
622
  }
583
623
  return result;
584
624
  }
625
+ function configureCatalogDatabase(db) {
626
+ db.exec(
627
+ "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;"
628
+ );
629
+ }
630
+ function initializeCatalogSchema(db) {
631
+ db.exec(`
632
+ CREATE TABLE IF NOT EXISTS catalog_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
633
+ CREATE TABLE IF NOT EXISTS sessions (
634
+ session_id TEXT PRIMARY KEY,
635
+ transcript_relative_path TEXT NOT NULL,
636
+ summary_relative_path TEXT NOT NULL,
637
+ summary_json TEXT NOT NULL,
638
+ transcript_size INTEGER NOT NULL DEFAULT 0,
639
+ transcript_mtime_ms REAL NOT NULL DEFAULT 0,
640
+ summary_revision INTEGER NOT NULL DEFAULT 1,
641
+ indexed_at TEXT NOT NULL,
642
+ damaged INTEGER NOT NULL DEFAULT 0
643
+ );
644
+ CREATE TABLE IF NOT EXISTS session_leases (
645
+ session_id TEXT PRIMARY KEY,
646
+ lease_id TEXT NOT NULL UNIQUE,
647
+ lease_secret_hash TEXT NOT NULL,
648
+ owner_instance_id TEXT NOT NULL,
649
+ owner_pid INTEGER NOT NULL,
650
+ owner_started_at TEXT NOT NULL,
651
+ entry_json TEXT NOT NULL,
652
+ agent_revision INTEGER NOT NULL DEFAULT 0,
653
+ status TEXT NOT NULL,
654
+ last_heartbeat_at INTEGER NOT NULL,
655
+ lease_expires_at INTEGER NOT NULL
656
+ );
657
+ CREATE TABLE IF NOT EXISTS resume_reservations (
658
+ reservation_id TEXT PRIMARY KEY,
659
+ target_session_id TEXT NOT NULL UNIQUE,
660
+ requester_instance_id TEXT NOT NULL,
661
+ current_session_id TEXT,
662
+ created_at INTEGER NOT NULL,
663
+ expires_at INTEGER NOT NULL
664
+ );
665
+ CREATE TABLE IF NOT EXISTS maintenance_leases (
666
+ session_id TEXT PRIMARY KEY,
667
+ operation TEXT NOT NULL,
668
+ holder_id TEXT NOT NULL,
669
+ lease_id TEXT NOT NULL UNIQUE,
670
+ acquired_at INTEGER NOT NULL,
671
+ expires_at INTEGER NOT NULL
672
+ );
673
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(indexed_at DESC);
674
+ CREATE INDEX IF NOT EXISTS idx_leases_expiry ON session_leases(lease_expires_at);
675
+ CREATE INDEX IF NOT EXISTS idx_reservations_expiry ON resume_reservations(expires_at);
676
+ CREATE INDEX IF NOT EXISTS idx_maintenance_expiry ON maintenance_leases(expires_at);
677
+ `);
678
+ db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("schema_version", String(SCHEMA_VERSION));
679
+ db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("generation", "0");
680
+ const row = db.prepare("SELECT value FROM catalog_meta WHERE key=?").get("schema_version");
681
+ if (Number(row.value) !== SCHEMA_VERSION)
682
+ throw new Error(`Unsupported session catalog schema ${row.value}`);
683
+ }
684
+
685
+ // src/session-catalog/store-rebuild.ts
686
+ function walkSessionFiles(root, suffix) {
687
+ const result = [];
688
+ const visit = (dir) => {
689
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
690
+ if (entry.isDirectory()) {
691
+ if (entry.name !== "_cas" && entry.name !== "_trash") visit(path2.join(dir, entry.name));
692
+ } else if (entry.isFile() && entry.name.endsWith(suffix))
693
+ result.push(path2.join(dir, entry.name));
694
+ }
695
+ };
696
+ visit(root);
697
+ return result;
698
+ }
699
+ function summarizeTranscriptFile(transcriptFilePath, id) {
700
+ const lines = fs.readFileSync(transcriptFilePath, "utf8").split(/\r?\n/);
701
+ let start;
702
+ let endedAt;
703
+ let lastActivityAt;
704
+ let messageCount = 0;
705
+ let iterationCount = 0;
706
+ let toolCallCount = 0;
707
+ let compactionCount = 0;
708
+ let tokenTotal = 0;
709
+ for (const line of lines) {
710
+ if (!line) continue;
711
+ let event;
712
+ try {
713
+ event = parseJson(line);
714
+ } catch {
715
+ continue;
716
+ }
717
+ lastActivityAt = event.ts;
718
+ if (event.type === "session_start") start = event;
719
+ if (event.type === "session_end") endedAt = event.ts;
720
+ if (event.type === "message_appended" && (event.message.role === "user" || event.message.role === "assistant"))
721
+ messageCount++;
722
+ if (event.type === "llm_response") {
723
+ iterationCount++;
724
+ tokenTotal += event.usage.input + event.usage.output + (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
725
+ }
726
+ if (event.type === "tool_call_end") toolCallCount++;
727
+ if (event.type === "compaction") compactionCount++;
728
+ }
729
+ if (!start) throw new Error("missing session_start");
730
+ return {
731
+ id,
732
+ title: id,
733
+ startedAt: start.ts,
734
+ ...endedAt ? { endedAt } : {},
735
+ model: start.model,
736
+ provider: start.provider,
737
+ tokenTotal,
738
+ ...lastActivityAt ? { lastActivityAt } : {},
739
+ messageCount,
740
+ iterationCount,
741
+ toolCallCount,
742
+ compactionCount
743
+ };
744
+ }
745
+ function rebuildCatalogIndex(db, sessionsDir, containedPathFn, transactionFn, bumpGenerationFn) {
746
+ const summaries = walkSessionFiles(sessionsDir, ".summary.json");
747
+ const transcripts = walkSessionFiles(sessionsDir, ".jsonl").filter(
748
+ (file) => isSessionTranscriptFileName(path2.basename(file))
749
+ );
750
+ const ids = /* @__PURE__ */ new Set();
751
+ for (const file of [...summaries, ...transcripts]) {
752
+ const relative3 = path2.relative(sessionsDir, file).replaceAll("\\", "/");
753
+ ids.add(relative3.replace(/\.summary\.json$|\.jsonl$/, ""));
754
+ }
755
+ let indexed = 0;
756
+ let damaged = 0;
757
+ transactionFn(() => {
758
+ db.prepare("DELETE FROM sessions").run();
759
+ for (const id of ids) {
760
+ try {
761
+ const summaryFile = containedPathFn(`${id}.summary.json`);
762
+ const summary = fs.existsSync(summaryFile) ? parseJson(fs.readFileSync(summaryFile, "utf8")) : summarizeTranscriptFile(containedPathFn(`${id}.jsonl`), id);
763
+ if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
764
+ const transcript = containedPathFn(`${id}.jsonl`);
765
+ const stat = fs.existsSync(transcript) ? fs.statSync(transcript) : void 0;
766
+ const now = (/* @__PURE__ */ new Date()).toISOString();
767
+ db.prepare(
768
+ "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)"
769
+ ).run(
770
+ id,
771
+ `${id}.jsonl`,
772
+ `${id}.summary.json`,
773
+ JSON.stringify(summary),
774
+ stat?.size ?? 0,
775
+ stat?.mtimeMs ?? 0,
776
+ 1,
777
+ now
778
+ );
779
+ indexed++;
780
+ } catch {
781
+ const now = (/* @__PURE__ */ new Date()).toISOString();
782
+ const fallback = {
783
+ id,
784
+ title: id,
785
+ startedAt: now,
786
+ model: "",
787
+ provider: "",
788
+ tokenTotal: 0
789
+ };
790
+ db.prepare(
791
+ "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,1,?,1)"
792
+ ).run(id, `${id}.jsonl`, `${id}.summary.json`, JSON.stringify(fallback), now);
793
+ damaged++;
794
+ }
795
+ }
796
+ db.prepare(
797
+ "INSERT INTO catalog_meta(key,value) VALUES ('last_reconciliation',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
798
+ ).run((/* @__PURE__ */ new Date()).toISOString());
799
+ bumpGenerationFn();
800
+ });
801
+ return { indexed, damaged };
802
+ }
803
+
804
+ // src/session-catalog/store.ts
585
805
  var SessionCatalogStore = class {
586
806
  constructor(projectDir) {
587
807
  this.projectDir = projectDir;
588
- this.sessionsDir = path2.join(projectDir, "sessions");
589
- fs.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
590
- this.databasePath = path2.join(this.sessionsDir, "catalog.sqlite");
808
+ this.sessionsDir = path3.join(projectDir, "sessions");
809
+ fs2.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
810
+ this.databasePath = path3.join(this.sessionsDir, "catalog.sqlite");
591
811
  const Database = loadDatabaseSync();
592
812
  this.db = new Database(this.databasePath);
593
813
  try {
@@ -604,12 +824,12 @@ var SessionCatalogStore = class {
604
824
  this.db.close();
605
825
  const quarantine = `${this.databasePath}.corrupt-${Date.now()}`;
606
826
  try {
607
- fs.renameSync(this.databasePath, quarantine);
827
+ fs2.renameSync(this.databasePath, quarantine);
608
828
  } catch {
609
829
  }
610
830
  for (const suffix of ["-wal", "-shm"]) {
611
831
  try {
612
- fs.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
832
+ fs2.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
613
833
  } catch {
614
834
  }
615
835
  }
@@ -621,7 +841,7 @@ var SessionCatalogStore = class {
621
841
  const rowCount = Number(
622
842
  this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get().count
623
843
  );
624
- if (rowCount === 0 && this.walkFiles(this.sessionsDir, ".jsonl").some((file) => !file.endsWith("_index.jsonl"))) {
844
+ if (rowCount === 0 && walkSessionFiles(this.sessionsDir, ".jsonl").some((file) => !file.endsWith("_index.jsonl"))) {
625
845
  this.rebuildCatalog();
626
846
  }
627
847
  }
@@ -631,66 +851,13 @@ var SessionCatalogStore = class {
631
851
  db;
632
852
  scrubber = new DefaultSecretScrubber();
633
853
  configureDatabase() {
634
- this.db.exec(
635
- "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;"
636
- );
854
+ configureCatalogDatabase(this.db);
637
855
  }
638
856
  close() {
639
857
  this.db.close();
640
858
  }
641
859
  initialize() {
642
- this.db.exec(`
643
- CREATE TABLE IF NOT EXISTS catalog_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
644
- CREATE TABLE IF NOT EXISTS sessions (
645
- session_id TEXT PRIMARY KEY,
646
- transcript_relative_path TEXT NOT NULL,
647
- summary_relative_path TEXT NOT NULL,
648
- summary_json TEXT NOT NULL,
649
- transcript_size INTEGER NOT NULL DEFAULT 0,
650
- transcript_mtime_ms REAL NOT NULL DEFAULT 0,
651
- summary_revision INTEGER NOT NULL DEFAULT 1,
652
- indexed_at TEXT NOT NULL,
653
- damaged INTEGER NOT NULL DEFAULT 0
654
- );
655
- CREATE TABLE IF NOT EXISTS session_leases (
656
- session_id TEXT PRIMARY KEY,
657
- lease_id TEXT NOT NULL UNIQUE,
658
- lease_secret_hash TEXT NOT NULL,
659
- owner_instance_id TEXT NOT NULL,
660
- owner_pid INTEGER NOT NULL,
661
- owner_started_at TEXT NOT NULL,
662
- entry_json TEXT NOT NULL,
663
- agent_revision INTEGER NOT NULL DEFAULT 0,
664
- status TEXT NOT NULL,
665
- last_heartbeat_at INTEGER NOT NULL,
666
- lease_expires_at INTEGER NOT NULL
667
- );
668
- CREATE TABLE IF NOT EXISTS resume_reservations (
669
- reservation_id TEXT PRIMARY KEY,
670
- target_session_id TEXT NOT NULL UNIQUE,
671
- requester_instance_id TEXT NOT NULL,
672
- current_session_id TEXT,
673
- created_at INTEGER NOT NULL,
674
- expires_at INTEGER NOT NULL
675
- );
676
- CREATE TABLE IF NOT EXISTS maintenance_leases (
677
- session_id TEXT PRIMARY KEY,
678
- operation TEXT NOT NULL,
679
- holder_id TEXT NOT NULL,
680
- lease_id TEXT NOT NULL UNIQUE,
681
- acquired_at INTEGER NOT NULL,
682
- expires_at INTEGER NOT NULL
683
- );
684
- CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(indexed_at DESC);
685
- CREATE INDEX IF NOT EXISTS idx_leases_expiry ON session_leases(lease_expires_at);
686
- CREATE INDEX IF NOT EXISTS idx_reservations_expiry ON resume_reservations(expires_at);
687
- CREATE INDEX IF NOT EXISTS idx_maintenance_expiry ON maintenance_leases(expires_at);
688
- `);
689
- this.db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("schema_version", String(SCHEMA_VERSION));
690
- this.db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("generation", "0");
691
- const row = this.db.prepare("SELECT value FROM catalog_meta WHERE key=?").get("schema_version");
692
- if (Number(row.value) !== SCHEMA_VERSION)
693
- throw new Error(`Unsupported session catalog schema ${row.value}`);
860
+ initializeCatalogSchema(this.db);
694
861
  }
695
862
  transaction(run) {
696
863
  this.db.exec("BEGIN IMMEDIATE");
@@ -731,21 +898,6 @@ var SessionCatalogStore = class {
731
898
  leaseRow(sessionId) {
732
899
  return this.db.prepare("SELECT * FROM session_leases WHERE session_id=?").get(sessionId);
733
900
  }
734
- /**
735
- * True when `sessionId` has a live `session_leases` row owned by a
736
- * different OS process than the *caller*. Used by
737
- * `acquireMaintenance` to distinguish "another running wstack" (which
738
- * must be fenced off) from "this very TUI owning its own session"
739
- * (which is allowed to claim a non-destructive maintenance lease on
740
- * its own transcript).
741
- *
742
- * `callerPid` must be the pid of the process that asked for the lease,
743
- * NOT `process.pid`. In the built runtime this store lives inside the
744
- * detached project-catalog daemon, so `process.pid` is the daemon's and
745
- * would mark every lease — including the caller's own session — foreign,
746
- * making `/clear` and `/rewind` fail with "Session … is live". Callers
747
- * that run the store in-process may omit it.
748
- */
749
901
  foreignLiveLease(sessionId, callerPid = process.pid) {
750
902
  const live = this.leaseRow(sessionId);
751
903
  return Boolean(live) && live.owner_pid !== callerPid;
@@ -835,7 +987,7 @@ var SessionCatalogStore = class {
835
987
  if (this.maintenanceExists(targetSessionId))
836
988
  throw conflict(`Session ${targetSessionId} is under maintenance`);
837
989
  const catalog = this.db.prepare("SELECT 1 AS yes FROM sessions WHERE session_id=?").get(targetSessionId);
838
- if (!catalog && !fs.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
990
+ if (!catalog && !fs2.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
839
991
  throw new Error(`Session not found: ${targetSessionId}`);
840
992
  const reservationId = randomUUID();
841
993
  const now = Date.now();
@@ -963,9 +1115,9 @@ var SessionCatalogStore = class {
963
1115
  }
964
1116
  containedPath(relative3) {
965
1117
  assertId(relative3.replace(/\.(jsonl|summary\.json)$/, ""), "session path");
966
- const root = path2.resolve(this.sessionsDir);
967
- const candidate = path2.resolve(root, relative3);
968
- const prefix = `${root}${path2.sep}`;
1118
+ const root = path3.resolve(this.sessionsDir);
1119
+ const candidate = path3.resolve(root, relative3);
1120
+ const prefix = `${root}${path3.sep}`;
969
1121
  if (candidate !== root && !(process.platform === "win32" ? candidate.toLowerCase().startsWith(prefix.toLowerCase()) : candidate.startsWith(prefix)))
970
1122
  throw new TypeError("Session path escapes sessions directory");
971
1123
  return candidate;
@@ -981,7 +1133,7 @@ var SessionCatalogStore = class {
981
1133
  transcriptRelativePath = normalizedTranscript;
982
1134
  summaryRelativePath = normalizedSummary;
983
1135
  const transcript = this.containedPath(transcriptRelativePath);
984
- const stat = fs.existsSync(transcript) ? fs.statSync(transcript) : void 0;
1136
+ const stat = fs2.existsSync(transcript) ? fs2.statSync(transcript) : void 0;
985
1137
  const now = (/* @__PURE__ */ new Date()).toISOString();
986
1138
  return this.transaction(() => {
987
1139
  const prior = this.db.prepare("SELECT summary_revision FROM sessions WHERE session_id=?").get(summary.id);
@@ -1104,7 +1256,7 @@ var SessionCatalogStore = class {
1104
1256
  if (trimmed) summary.name = this.scrubber.scrub(trimmed).slice(0, 500);
1105
1257
  else delete summary.name;
1106
1258
  const summaryPath = this.containedPath(current.summaryRelativePath);
1107
- fs.mkdirSync(path2.dirname(summaryPath), { recursive: true, mode: 448 });
1259
+ fs2.mkdirSync(path3.dirname(summaryPath), { recursive: true, mode: 448 });
1108
1260
  await atomicWrite(summaryPath, `${JSON.stringify(summary)}
1109
1261
  `, { mode: 384 });
1110
1262
  try {
@@ -1164,16 +1316,16 @@ var SessionCatalogStore = class {
1164
1316
  this.containedPath(`${sessionId}.plan.json`),
1165
1317
  this.containedPath(`${sessionId}.tasks.json`),
1166
1318
  this.containedPath(`${sessionId}.todos.json`),
1167
- path2.join(path2.dirname(transcript), path2.basename(sessionId))
1319
+ path3.join(path3.dirname(transcript), path3.basename(sessionId))
1168
1320
  ];
1169
- const trashRoot = path2.join(this.sessionsDir, "_trash", lease.leaseId);
1170
- fs.mkdirSync(trashRoot, { recursive: true, mode: 448 });
1321
+ const trashRoot = path3.join(this.sessionsDir, "_trash", lease.leaseId);
1322
+ fs2.mkdirSync(trashRoot, { recursive: true, mode: 448 });
1171
1323
  const moved = [];
1172
1324
  try {
1173
1325
  artifacts.forEach((artifact, index) => {
1174
- if (!fs.existsSync(artifact)) return;
1175
- const target = path2.join(trashRoot, `${index}-${path2.basename(artifact)}`);
1176
- fs.renameSync(artifact, target);
1326
+ if (!fs2.existsSync(artifact)) return;
1327
+ const target = path3.join(trashRoot, `${index}-${path3.basename(artifact)}`);
1328
+ fs2.renameSync(artifact, target);
1177
1329
  moved.push({ from: artifact, to: target });
1178
1330
  });
1179
1331
  this.transaction(() => {
@@ -1188,17 +1340,17 @@ var SessionCatalogStore = class {
1188
1340
  } catch (error) {
1189
1341
  for (const item of moved.reverse()) {
1190
1342
  try {
1191
- fs.mkdirSync(path2.dirname(item.from), { recursive: true, mode: 448 });
1192
- fs.renameSync(item.to, item.from);
1343
+ fs2.mkdirSync(path3.dirname(item.from), { recursive: true, mode: 448 });
1344
+ fs2.renameSync(item.to, item.from);
1193
1345
  } catch {
1194
1346
  }
1195
1347
  }
1196
1348
  throw error;
1197
1349
  }
1198
1350
  try {
1199
- fs.rmSync(trashRoot, { recursive: true, force: true });
1200
- const trashParent = path2.dirname(trashRoot);
1201
- if (fs.readdirSync(trashParent).length === 0) fs.rmdirSync(trashParent);
1351
+ fs2.rmSync(trashRoot, { recursive: true, force: true });
1352
+ const trashParent = path3.dirname(trashRoot);
1353
+ if (fs2.readdirSync(trashParent).length === 0) fs2.rmdirSync(trashParent);
1202
1354
  } catch {
1203
1355
  }
1204
1356
  }
@@ -1219,122 +1371,13 @@ var SessionCatalogStore = class {
1219
1371
  return deleted;
1220
1372
  }
1221
1373
  rebuildCatalog() {
1222
- const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
1223
- const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
1224
- (file) => isSessionTranscriptFileName(path2.basename(file))
1374
+ return rebuildCatalogIndex(
1375
+ this.db,
1376
+ this.sessionsDir,
1377
+ (rel) => this.containedPath(rel),
1378
+ (run) => this.transaction(run),
1379
+ () => this.bumpGeneration()
1225
1380
  );
1226
- const ids = /* @__PURE__ */ new Set();
1227
- for (const file of [...summaries, ...transcripts]) {
1228
- const relative3 = path2.relative(this.sessionsDir, file).replaceAll("\\", "/");
1229
- ids.add(relative3.replace(/\.summary\.json$|\.jsonl$/, ""));
1230
- }
1231
- let indexed = 0;
1232
- let damaged = 0;
1233
- this.transaction(() => {
1234
- this.db.prepare("DELETE FROM sessions").run();
1235
- for (const id of ids) {
1236
- try {
1237
- const summaryFile = this.containedPath(`${id}.summary.json`);
1238
- const summary = fs.existsSync(summaryFile) ? parseJson(fs.readFileSync(summaryFile, "utf8")) : this.summarizeTranscript(id);
1239
- if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
1240
- const transcript = this.containedPath(`${id}.jsonl`);
1241
- const stat = fs.existsSync(transcript) ? fs.statSync(transcript) : void 0;
1242
- const now = (/* @__PURE__ */ new Date()).toISOString();
1243
- this.db.prepare(
1244
- "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)"
1245
- ).run(
1246
- id,
1247
- `${id}.jsonl`,
1248
- `${id}.summary.json`,
1249
- JSON.stringify(summary),
1250
- stat?.size ?? 0,
1251
- stat?.mtimeMs ?? 0,
1252
- 1,
1253
- now
1254
- );
1255
- indexed++;
1256
- } catch {
1257
- const now = (/* @__PURE__ */ new Date()).toISOString();
1258
- const fallback = {
1259
- id,
1260
- title: id,
1261
- startedAt: now,
1262
- model: "",
1263
- provider: "",
1264
- tokenTotal: 0
1265
- };
1266
- this.db.prepare(
1267
- "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,1,?,1)"
1268
- ).run(id, `${id}.jsonl`, `${id}.summary.json`, JSON.stringify(fallback), now);
1269
- damaged++;
1270
- }
1271
- }
1272
- this.db.prepare(
1273
- "INSERT INTO catalog_meta(key,value) VALUES ('last_reconciliation',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
1274
- ).run((/* @__PURE__ */ new Date()).toISOString());
1275
- this.bumpGeneration();
1276
- });
1277
- return { indexed, damaged };
1278
- }
1279
- walkFiles(root, suffix) {
1280
- const result = [];
1281
- const visit = (dir) => {
1282
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1283
- if (entry.isDirectory()) {
1284
- if (entry.name !== "_cas" && entry.name !== "_trash") visit(path2.join(dir, entry.name));
1285
- } else if (entry.isFile() && entry.name.endsWith(suffix))
1286
- result.push(path2.join(dir, entry.name));
1287
- }
1288
- };
1289
- visit(root);
1290
- return result;
1291
- }
1292
- summarizeTranscript(id) {
1293
- const file = this.containedPath(`${id}.jsonl`);
1294
- const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
1295
- let start;
1296
- let endedAt;
1297
- let lastActivityAt;
1298
- let messageCount = 0;
1299
- let iterationCount = 0;
1300
- let toolCallCount = 0;
1301
- let compactionCount = 0;
1302
- let tokenTotal = 0;
1303
- for (const line of lines) {
1304
- if (!line) continue;
1305
- let event;
1306
- try {
1307
- event = parseJson(line);
1308
- } catch {
1309
- continue;
1310
- }
1311
- lastActivityAt = event.ts;
1312
- if (event.type === "session_start") start = event;
1313
- if (event.type === "session_end") endedAt = event.ts;
1314
- if (event.type === "message_appended" && (event.message.role === "user" || event.message.role === "assistant"))
1315
- messageCount++;
1316
- if (event.type === "llm_response") {
1317
- iterationCount++;
1318
- tokenTotal += event.usage.input + event.usage.output + (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
1319
- }
1320
- if (event.type === "tool_call_end") toolCallCount++;
1321
- if (event.type === "compaction") compactionCount++;
1322
- }
1323
- if (!start) throw new Error("missing session_start");
1324
- return {
1325
- id,
1326
- title: id,
1327
- startedAt: start.ts,
1328
- ...endedAt ? { endedAt } : {},
1329
- model: start.model,
1330
- provider: start.provider,
1331
- tokenTotal,
1332
- ...lastActivityAt ? { lastActivityAt } : {},
1333
- messageCount,
1334
- iterationCount,
1335
- toolCallCount,
1336
- compactionCount
1337
- };
1338
1381
  }
1339
1382
  health(base) {
1340
1383
  this.reapExpired();
@@ -1366,13 +1409,13 @@ function parseArgs(argv) {
1366
1409
  const projectRoot = values.get("--project-root");
1367
1410
  if (!projectDir) throw new Error("Session Catalog project server requires --project-dir");
1368
1411
  if (!projectRoot) throw new Error("Session Catalog project server requires --project-root");
1369
- return { projectDir: path3.resolve(projectDir), projectRoot: path3.resolve(projectRoot) };
1412
+ return { projectDir: path4.resolve(projectDir), projectRoot: path4.resolve(projectRoot) };
1370
1413
  }
1371
1414
  useDaemonPerfDefaults();
1372
1415
  var parsed = parseArgs(process.argv.slice(2));
1373
1416
  var endpoint = sessionCatalogProjectServerEndpoint(parsed.projectDir);
1374
1417
  var metadataPath = sessionCatalogProjectServerMetadataPath(parsed.projectDir);
1375
- var databasePath = path3.join(parsed.projectDir, "sessions", "catalog.sqlite");
1418
+ var databasePath = path4.join(parsed.projectDir, "sessions", "catalog.sqlite");
1376
1419
  var startedAt = (/* @__PURE__ */ new Date()).toISOString();
1377
1420
  var instanceId = randomUUID2();
1378
1421
  var authToken = randomBytes2(32).toString("hex");
@@ -1389,7 +1432,7 @@ var serverInfo = {
1389
1432
  instanceId,
1390
1433
  startedAt
1391
1434
  };
1392
- process.title = `wrongstack-session-catalog:${path3.basename(parsed.projectRoot)}`;
1435
+ process.title = `wrongstack-session-catalog:${path4.basename(parsed.projectRoot)}`;
1393
1436
  var store;
1394
1437
  var activeRequests = 0;
1395
1438
  var stopping = false;
@@ -1493,13 +1536,13 @@ function validateOperationArgs(op, args) {
1493
1536
  if (record["entry"] !== void 0) exact(record["entry"], `${op}.entry`, entryKeys);
1494
1537
  if (record["entry"] && typeof record["entry"] === "object") {
1495
1538
  const entry = record["entry"];
1496
- if (typeof entry["projectRoot"] !== "string" || (process.platform === "win32" ? path3.resolve(entry["projectRoot"]).toLowerCase() !== parsed.projectRoot.toLowerCase() : path3.resolve(entry["projectRoot"]) !== parsed.projectRoot)) {
1539
+ if (typeof entry["projectRoot"] !== "string" || (process.platform === "win32" ? path4.resolve(entry["projectRoot"]).toLowerCase() !== parsed.projectRoot.toLowerCase() : path4.resolve(entry["projectRoot"]) !== parsed.projectRoot)) {
1497
1540
  throw new TypeError(`${op}.entry project identity does not match this daemon`);
1498
1541
  }
1499
1542
  if (typeof entry["workingDir"] !== "string")
1500
1543
  throw new TypeError(`${op}.entry workingDir is required`);
1501
- const relative3 = path3.relative(parsed.projectRoot, path3.resolve(entry["workingDir"]));
1502
- if (relative3 === ".." || relative3.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative3)) {
1544
+ const relative3 = path4.relative(parsed.projectRoot, path4.resolve(entry["workingDir"]));
1545
+ if (relative3 === ".." || relative3.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative3)) {
1503
1546
  throw new TypeError(`${op}.entry workingDir is outside the project root`);
1504
1547
  }
1505
1548
  }