@wrongstack/core 0.302.2 → 0.305.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 (97) hide show
  1. package/README.md +1 -1
  2. package/dist/chronicle/index.js +117 -30
  3. package/dist/chronicle/project-server.js +12 -5
  4. package/dist/coordination/agents/index.js +4645 -3448
  5. package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
  6. package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
  7. package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
  8. package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
  9. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  10. package/dist/coordination/agents/project-agent-files.d.ts +12 -3
  11. package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
  12. package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
  13. package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
  14. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  15. package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
  16. package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
  17. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  18. package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
  19. package/dist/coordination/agents/role-skills.d.ts +11 -1
  20. package/dist/coordination/agents/types.d.ts +10 -2
  21. package/dist/coordination/director-prompts.d.ts +19 -6
  22. package/dist/coordination/director-tools.d.ts +2 -2
  23. package/dist/coordination/fleet.d.ts +0 -6
  24. package/dist/coordination/index.d.ts +1 -1
  25. package/dist/coordination/index.js +5746 -4326
  26. package/dist/coordination/mail-tools.d.ts +1 -1
  27. package/dist/core/agent-types.d.ts +4 -2
  28. package/dist/core/agent.d.ts +1 -0
  29. package/dist/core/context.d.ts +19 -0
  30. package/dist/core/conversation-state.d.ts +14 -0
  31. package/dist/core/fallback-profile-manager.d.ts +70 -2
  32. package/dist/core/index.js +317 -108
  33. package/dist/core/system-prompt-blocks.d.ts +1 -1
  34. package/dist/core/system-prompt-builder.d.ts +13 -1
  35. package/dist/core/system-prompt-glossary.d.ts +73 -0
  36. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  37. package/dist/defaults/index.js +1635 -1120
  38. package/dist/execution/council-orchestrator.d.ts +3 -13
  39. package/dist/execution/index.js +3136 -2706
  40. package/dist/execution/one-shot-llm.d.ts +5 -0
  41. package/dist/goal/index.js +7 -0
  42. package/dist/hq/index.js +17 -7
  43. package/dist/hq/protocol/kanban.d.ts +21 -0
  44. package/dist/hq/protocol.js +5 -1
  45. package/dist/hq/redaction.d.ts +14 -0
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +5502 -3483
  48. package/dist/infrastructure/index.js +247 -122
  49. package/dist/kernel/events/agent-events.d.ts +28 -0
  50. package/dist/plugin/index.js +111 -7
  51. package/dist/registry/index.js +11 -0
  52. package/dist/registry/tool-registry.d.ts +8 -0
  53. package/dist/replay/hash.d.ts +9 -0
  54. package/dist/replay/index.js +14 -4
  55. package/dist/replay/replay-provider-runner.d.ts +31 -1
  56. package/dist/security/index.js +92 -21
  57. package/dist/security/kanban-boundary.d.ts +5 -1
  58. package/dist/security/secret-vault.d.ts +2 -0
  59. package/dist/session-catalog/index.js +86 -10
  60. package/dist/session-catalog/project-server.js +100 -17
  61. package/dist/session-catalog/protocol.d.ts +20 -4
  62. package/dist/session-catalog/store.d.ts +19 -3
  63. package/dist/storage/index.js +305 -69
  64. package/dist/storage/memory-consolidator.d.ts +4 -2
  65. package/dist/storage/plan-store.d.ts +1 -1
  66. package/dist/storage/session-resume-validation.d.ts +24 -0
  67. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  68. package/dist/storage/session-store/fork-session.d.ts +13 -1
  69. package/dist/storage/session-store/load-cache.d.ts +11 -0
  70. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  71. package/dist/storage/session-store.d.ts +18 -0
  72. package/dist/tasking/index.js +5 -0
  73. package/dist/tools/index.js +1151 -831
  74. package/dist/types/config/mcp-features.d.ts +31 -1
  75. package/dist/types/config/root.d.ts +23 -1
  76. package/dist/types/config/skills-fleet-brain.d.ts +34 -0
  77. package/dist/types/config/tools.d.ts +22 -0
  78. package/dist/types/config/ui.d.ts +14 -0
  79. package/dist/types/config.d.ts +1 -0
  80. package/dist/types/default-config.d.ts +1 -0
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +23 -0
  83. package/dist/types/multi-agent.d.ts +7 -0
  84. package/dist/types/session.d.ts +9 -1
  85. package/dist/types/task-graph.d.ts +2 -0
  86. package/dist/types/tool-executor.d.ts +2 -0
  87. package/dist/utils/index.d.ts +1 -0
  88. package/dist/utils/index.js +217 -76
  89. package/dist/utils/project-state-guard.d.ts +21 -0
  90. package/dist/utils/session-scoped-path.d.ts +17 -0
  91. package/dist/utils/todos-format.d.ts +20 -0
  92. package/instructions/leader-after-task.md +3 -4
  93. package/instructions/system-lite.md +17 -14
  94. package/instructions/system-pro.md +29 -30
  95. package/instructions/system.md +29 -28
  96. package/package.json +3 -3
  97. package/skills/wrongstack-kanban/SKILL.md +95 -93
@@ -1198,6 +1198,22 @@ function isPidAlive(pid) {
1198
1198
  }
1199
1199
  }
1200
1200
 
1201
+ // src/utils/session-scoped-path.ts
1202
+ var SESSION_SIDECAR_JSONL_SUFFIXES = [
1203
+ ".replay.jsonl",
1204
+ ".audit.jsonl",
1205
+ ".annotations.jsonl"
1206
+ ];
1207
+ var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
1208
+ "_index.jsonl",
1209
+ "_mailbox.jsonl"
1210
+ ]);
1211
+ function isSessionTranscriptFileName(name) {
1212
+ if (!name.endsWith(".jsonl")) return false;
1213
+ if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
1214
+ return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
1215
+ }
1216
+
1201
1217
  // src/session-catalog/store.ts
1202
1218
  var SCHEMA_VERSION = 1;
1203
1219
  var MAX_LEASE_MS = 12e4;
@@ -1392,6 +1408,25 @@ var SessionCatalogStore = class {
1392
1408
  leaseRow(sessionId) {
1393
1409
  return this.db.prepare("SELECT * FROM session_leases WHERE session_id=?").get(sessionId);
1394
1410
  }
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
+ foreignLiveLease(sessionId, callerPid = process.pid) {
1427
+ const live = this.leaseRow(sessionId);
1428
+ return Boolean(live) && live.owner_pid !== callerPid;
1429
+ }
1395
1430
  verifyCredential(credential) {
1396
1431
  assertId(credential.sessionId);
1397
1432
  const row = this.leaseRow(credential.sessionId);
@@ -1664,13 +1699,51 @@ var SessionCatalogStore = class {
1664
1699
  damaged: row.damaged !== 0
1665
1700
  };
1666
1701
  }
1667
- listCatalog(limit = 100, search) {
1668
- const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(limit)));
1669
- const rows = search?.trim() ? this.db.prepare(
1670
- "SELECT * FROM sessions WHERE session_id LIKE ? OR json_extract(summary_json,'$.title') LIKE ? OR json_extract(summary_json,'$.name') LIKE ? ORDER BY COALESCE(json_extract(summary_json,'$.lastActivityAt'),json_extract(summary_json,'$.startedAt')) DESC LIMIT ?"
1671
- ).all(`%${search.trim()}%`, `%${search.trim()}%`, `%${search.trim()}%`, bounded) : this.db.prepare(
1672
- "SELECT * FROM sessions ORDER BY COALESCE(json_extract(summary_json,'$.lastActivityAt'),json_extract(summary_json,'$.startedAt')) DESC LIMIT ?"
1673
- ).all(bounded);
1702
+ listCatalog(criteria = {}) {
1703
+ const requestedLimit = criteria.limit ?? 100;
1704
+ if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
1705
+ const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
1706
+ const clauses = [];
1707
+ const values = [];
1708
+ const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
1709
+ const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
1710
+ const search = criteria.search?.trim();
1711
+ if (search) {
1712
+ const pattern = literalLike(search);
1713
+ clauses.push(
1714
+ `(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
1715
+ );
1716
+ values.push(pattern, pattern, pattern);
1717
+ }
1718
+ if (criteria.since) {
1719
+ clauses.push(`${jsonText("startedAt")}>=?`);
1720
+ values.push(criteria.since);
1721
+ }
1722
+ if (criteria.until) {
1723
+ clauses.push(`${jsonText("startedAt")}<=?`);
1724
+ values.push(criteria.until);
1725
+ }
1726
+ if (criteria.provider) {
1727
+ clauses.push(`${jsonText("provider")}=?`);
1728
+ values.push(criteria.provider);
1729
+ }
1730
+ if (criteria.model) {
1731
+ clauses.push(`${jsonText("model")}=?`);
1732
+ values.push(criteria.model);
1733
+ }
1734
+ if (criteria.minTokens !== void 0) {
1735
+ if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
1736
+ clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
1737
+ values.push(criteria.minTokens);
1738
+ }
1739
+ if (criteria.titleContains) {
1740
+ clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
1741
+ values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
1742
+ }
1743
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
1744
+ const rows = this.db.prepare(
1745
+ `SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
1746
+ ).all(...values, bounded);
1674
1747
  return rows.map((row) => this.catalogRecord(row));
1675
1748
  }
1676
1749
  getSummary(sessionId) {
@@ -1725,11 +1798,14 @@ var SessionCatalogStore = class {
1725
1798
  throw error;
1726
1799
  }
1727
1800
  }
1728
- acquireMaintenance(sessionId, operation, holderId, leaseMs) {
1801
+ acquireMaintenance(sessionId, operation, holderId, leaseMs, holderPid) {
1729
1802
  assertId(sessionId);
1730
1803
  return this.transaction(() => {
1731
1804
  this.reapExpired();
1732
- if (this.leaseRow(sessionId)) throw conflict(`Session ${sessionId} is live`);
1805
+ const live = this.leaseRow(sessionId);
1806
+ if (live && (operation === "delete" || this.foreignLiveLease(sessionId, holderPid))) {
1807
+ throw conflict(`Session ${sessionId} is live`);
1808
+ }
1733
1809
  const reservation = this.db.prepare(
1734
1810
  "SELECT 1 AS yes FROM resume_reservations WHERE target_session_id=? AND expires_at>?"
1735
1811
  ).get(sessionId, Date.now());
@@ -1822,7 +1898,7 @@ var SessionCatalogStore = class {
1822
1898
  rebuildCatalog() {
1823
1899
  const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
1824
1900
  const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
1825
- (file) => !file.endsWith("_index.jsonl")
1901
+ (file) => isSessionTranscriptFileName(path4.basename(file))
1826
1902
  );
1827
1903
  const ids = /* @__PURE__ */ new Set();
1828
1904
  for (const file of [...summaries, ...transcripts]) {
@@ -567,6 +567,22 @@ function isPidAlive(pid) {
567
567
  }
568
568
  }
569
569
 
570
+ // src/utils/session-scoped-path.ts
571
+ var SESSION_SIDECAR_JSONL_SUFFIXES = [
572
+ ".replay.jsonl",
573
+ ".audit.jsonl",
574
+ ".annotations.jsonl"
575
+ ];
576
+ var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
577
+ "_index.jsonl",
578
+ "_mailbox.jsonl"
579
+ ]);
580
+ function isSessionTranscriptFileName(name) {
581
+ if (!name.endsWith(".jsonl")) return false;
582
+ if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
583
+ return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
584
+ }
585
+
570
586
  // src/session-catalog/store.ts
571
587
  var SCHEMA_VERSION = 1;
572
588
  var MAX_LEASE_MS = 12e4;
@@ -761,6 +777,25 @@ var SessionCatalogStore = class {
761
777
  leaseRow(sessionId) {
762
778
  return this.db.prepare("SELECT * FROM session_leases WHERE session_id=?").get(sessionId);
763
779
  }
780
+ /**
781
+ * True when `sessionId` has a live `session_leases` row owned by a
782
+ * different OS process than the *caller*. Used by
783
+ * `acquireMaintenance` to distinguish "another running wstack" (which
784
+ * must be fenced off) from "this very TUI owning its own session"
785
+ * (which is allowed to claim a non-destructive maintenance lease on
786
+ * its own transcript).
787
+ *
788
+ * `callerPid` must be the pid of the process that asked for the lease,
789
+ * NOT `process.pid`. In the built runtime this store lives inside the
790
+ * detached project-catalog daemon, so `process.pid` is the daemon's and
791
+ * would mark every lease — including the caller's own session — foreign,
792
+ * making `/clear` and `/rewind` fail with "Session … is live". Callers
793
+ * that run the store in-process may omit it.
794
+ */
795
+ foreignLiveLease(sessionId, callerPid = process.pid) {
796
+ const live = this.leaseRow(sessionId);
797
+ return Boolean(live) && live.owner_pid !== callerPid;
798
+ }
764
799
  verifyCredential(credential) {
765
800
  assertId(credential.sessionId);
766
801
  const row = this.leaseRow(credential.sessionId);
@@ -1033,13 +1068,51 @@ var SessionCatalogStore = class {
1033
1068
  damaged: row.damaged !== 0
1034
1069
  };
1035
1070
  }
1036
- listCatalog(limit = 100, search) {
1037
- const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(limit)));
1038
- const rows = search?.trim() ? this.db.prepare(
1039
- "SELECT * FROM sessions WHERE session_id LIKE ? OR json_extract(summary_json,'$.title') LIKE ? OR json_extract(summary_json,'$.name') LIKE ? ORDER BY COALESCE(json_extract(summary_json,'$.lastActivityAt'),json_extract(summary_json,'$.startedAt')) DESC LIMIT ?"
1040
- ).all(`%${search.trim()}%`, `%${search.trim()}%`, `%${search.trim()}%`, bounded) : this.db.prepare(
1041
- "SELECT * FROM sessions ORDER BY COALESCE(json_extract(summary_json,'$.lastActivityAt'),json_extract(summary_json,'$.startedAt')) DESC LIMIT ?"
1042
- ).all(bounded);
1071
+ listCatalog(criteria = {}) {
1072
+ const requestedLimit = criteria.limit ?? 100;
1073
+ if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
1074
+ const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
1075
+ const clauses = [];
1076
+ const values = [];
1077
+ const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
1078
+ const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
1079
+ const search = criteria.search?.trim();
1080
+ if (search) {
1081
+ const pattern = literalLike(search);
1082
+ clauses.push(
1083
+ `(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
1084
+ );
1085
+ values.push(pattern, pattern, pattern);
1086
+ }
1087
+ if (criteria.since) {
1088
+ clauses.push(`${jsonText("startedAt")}>=?`);
1089
+ values.push(criteria.since);
1090
+ }
1091
+ if (criteria.until) {
1092
+ clauses.push(`${jsonText("startedAt")}<=?`);
1093
+ values.push(criteria.until);
1094
+ }
1095
+ if (criteria.provider) {
1096
+ clauses.push(`${jsonText("provider")}=?`);
1097
+ values.push(criteria.provider);
1098
+ }
1099
+ if (criteria.model) {
1100
+ clauses.push(`${jsonText("model")}=?`);
1101
+ values.push(criteria.model);
1102
+ }
1103
+ if (criteria.minTokens !== void 0) {
1104
+ if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
1105
+ clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
1106
+ values.push(criteria.minTokens);
1107
+ }
1108
+ if (criteria.titleContains) {
1109
+ clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
1110
+ values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
1111
+ }
1112
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
1113
+ const rows = this.db.prepare(
1114
+ `SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
1115
+ ).all(...values, bounded);
1043
1116
  return rows.map((row) => this.catalogRecord(row));
1044
1117
  }
1045
1118
  getSummary(sessionId) {
@@ -1094,11 +1167,14 @@ var SessionCatalogStore = class {
1094
1167
  throw error;
1095
1168
  }
1096
1169
  }
1097
- acquireMaintenance(sessionId, operation, holderId, leaseMs) {
1170
+ acquireMaintenance(sessionId, operation, holderId, leaseMs, holderPid) {
1098
1171
  assertId(sessionId);
1099
1172
  return this.transaction(() => {
1100
1173
  this.reapExpired();
1101
- if (this.leaseRow(sessionId)) throw conflict(`Session ${sessionId} is live`);
1174
+ const live = this.leaseRow(sessionId);
1175
+ if (live && (operation === "delete" || this.foreignLiveLease(sessionId, holderPid))) {
1176
+ throw conflict(`Session ${sessionId} is live`);
1177
+ }
1102
1178
  const reservation = this.db.prepare(
1103
1179
  "SELECT 1 AS yes FROM resume_reservations WHERE target_session_id=? AND expires_at>?"
1104
1180
  ).get(sessionId, Date.now());
@@ -1191,7 +1267,7 @@ var SessionCatalogStore = class {
1191
1267
  rebuildCatalog() {
1192
1268
  const summaries = this.walkFiles(this.sessionsDir, ".summary.json");
1193
1269
  const transcripts = this.walkFiles(this.sessionsDir, ".jsonl").filter(
1194
- (file) => !file.endsWith("_index.jsonl")
1270
+ (file) => isSessionTranscriptFileName(path2.basename(file))
1195
1271
  );
1196
1272
  const ids = /* @__PURE__ */ new Set();
1197
1273
  for (const file of [...summaries, ...transcripts]) {
@@ -1400,11 +1476,20 @@ var OPERATION_KEYS = {
1400
1476
  subscribe: ["cursor"],
1401
1477
  unsubscribe: [],
1402
1478
  upsert_summary: ["summary", "transcriptRelativePath", "summaryRelativePath"],
1403
- list_catalog: ["limit", "search"],
1479
+ list_catalog: [
1480
+ "limit",
1481
+ "search",
1482
+ "since",
1483
+ "until",
1484
+ "provider",
1485
+ "model",
1486
+ "minTokens",
1487
+ "titleContains"
1488
+ ],
1404
1489
  resolve_id: ["query"],
1405
1490
  get_summary: ["sessionId"],
1406
1491
  rename: ["sessionId", "name"],
1407
- acquire_maintenance: ["sessionId", "operation", "holderId", "leaseMs"],
1492
+ acquire_maintenance: ["sessionId", "operation", "holderId", "leaseMs", "holderPid"],
1408
1493
  release_maintenance: ["lease"],
1409
1494
  delete: ["sessionId", "lease"],
1410
1495
  prune: ["maxAgeDays", "holderId"],
@@ -1558,10 +1643,7 @@ async function dispatch(op, args) {
1558
1643
  }
1559
1644
  case "list_catalog": {
1560
1645
  const value = args;
1561
- return catalog.listCatalog(
1562
- value.limit,
1563
- value.search
1564
- );
1646
+ return catalog.listCatalog(value);
1565
1647
  }
1566
1648
  case "resolve_id":
1567
1649
  return catalog.resolveId(
@@ -1584,7 +1666,8 @@ async function dispatch(op, args) {
1584
1666
  value.sessionId,
1585
1667
  value.operation,
1586
1668
  value.holderId,
1587
- value.leaseMs
1669
+ value.leaseMs,
1670
+ value.holderPid
1588
1671
  );
1589
1672
  }
1590
1673
  case "release_maintenance": {
@@ -67,6 +67,16 @@ export interface CatalogSessionRecord extends SessionSummary {
67
67
  indexedAt: string;
68
68
  damaged: boolean;
69
69
  }
70
+ export interface SessionCatalogListArgs {
71
+ limit?: number | undefined;
72
+ search?: string | undefined;
73
+ since?: string | undefined;
74
+ until?: string | undefined;
75
+ provider?: string | undefined;
76
+ model?: string | undefined;
77
+ minTokens?: number | undefined;
78
+ titleContains?: string | undefined;
79
+ }
70
80
  export type SessionCatalogEventKind = 'session.claimed' | 'session.presence_changed' | 'session.closing' | 'session.released' | 'session.catalog_changed' | 'session.deleted' | 'session.rebuild_started' | 'session.rebuild_completed';
71
81
  export interface SessionCatalogEvent {
72
82
  instanceId: string;
@@ -76,6 +86,12 @@ export interface SessionCatalogEvent {
76
86
  generation: number;
77
87
  at: string;
78
88
  }
89
+ /**
90
+ * NOTE: adding a field to any `args` below also requires adding it to
91
+ * `OPERATION_KEYS` in `project-server.ts` — the daemon validates every
92
+ * request against that allowlist and rejects unknown fields at runtime,
93
+ * which TypeScript cannot catch.
94
+ */
79
95
  export interface SessionCatalogOperations {
80
96
  ping: {
81
97
  args: Record<string, never>;
@@ -179,10 +195,7 @@ export interface SessionCatalogOperations {
179
195
  result: CatalogSessionRecord;
180
196
  };
181
197
  list_catalog: {
182
- args: {
183
- limit?: number;
184
- search?: string;
185
- };
198
+ args: SessionCatalogListArgs;
186
199
  result: CatalogSessionRecord[];
187
200
  };
188
201
  resolve_id: {
@@ -210,6 +223,9 @@ export interface SessionCatalogOperations {
210
223
  operation: MaintenanceLease['operation'];
211
224
  holderId: string;
212
225
  leaseMs?: number;
226
+ /** Caller's OS pid. The server runs in the detached catalog daemon, so
227
+ * it cannot use its own `process.pid` to recognise the session owner. */
228
+ holderPid?: number;
213
229
  };
214
230
  result: MaintenanceLease;
215
231
  };
@@ -1,6 +1,6 @@
1
1
  import type { SessionRegistryEntry } from '../session-registry-types.js';
2
2
  import type { SessionSummary } from '../types/session.js';
3
- import type { CatalogSessionRecord, MaintenanceLease, ResumeReservation, SessionCatalogHealth, SessionLeaseCredential } from './protocol.js';
3
+ import type { CatalogSessionRecord, MaintenanceLease, ResumeReservation, SessionCatalogHealth, SessionCatalogListArgs, SessionLeaseCredential } from './protocol.js';
4
4
  export declare class SessionCatalogStore {
5
5
  readonly projectDir: string;
6
6
  readonly databasePath: string;
@@ -17,6 +17,22 @@ export declare class SessionCatalogStore {
17
17
  private reapExpired;
18
18
  private maintenanceExists;
19
19
  private leaseRow;
20
+ /**
21
+ * True when `sessionId` has a live `session_leases` row owned by a
22
+ * different OS process than the *caller*. Used by
23
+ * `acquireMaintenance` to distinguish "another running wstack" (which
24
+ * must be fenced off) from "this very TUI owning its own session"
25
+ * (which is allowed to claim a non-destructive maintenance lease on
26
+ * its own transcript).
27
+ *
28
+ * `callerPid` must be the pid of the process that asked for the lease,
29
+ * NOT `process.pid`. In the built runtime this store lives inside the
30
+ * detached project-catalog daemon, so `process.pid` is the daemon's and
31
+ * would mark every lease — including the caller's own session — foreign,
32
+ * making `/clear` and `/rewind` fail with "Session … is live". Callers
33
+ * that run the store in-process may omit it.
34
+ */
35
+ private foreignLiveLease;
20
36
  private verifyCredential;
21
37
  private createLease;
22
38
  claimNew(entry: SessionRegistryEntry, ownerInstanceId: string, leaseMs?: number): SessionLeaseCredential;
@@ -36,11 +52,11 @@ export declare class SessionCatalogStore {
36
52
  private containedPath;
37
53
  upsertSummary(summary: SessionSummary, transcriptRelativePath?: string, summaryRelativePath?: string): CatalogSessionRecord;
38
54
  private catalogRecord;
39
- listCatalog(limit?: number, search?: string): CatalogSessionRecord[];
55
+ listCatalog(criteria?: SessionCatalogListArgs): CatalogSessionRecord[];
40
56
  getSummary(sessionId: string): CatalogSessionRecord | null;
41
57
  resolveId(query: string): string;
42
58
  rename(sessionId: string, name: string): Promise<CatalogSessionRecord>;
43
- acquireMaintenance(sessionId: string, operation: MaintenanceLease['operation'], holderId: string, leaseMs?: number): MaintenanceLease;
59
+ acquireMaintenance(sessionId: string, operation: MaintenanceLease['operation'], holderId: string, leaseMs?: number, holderPid?: number): MaintenanceLease;
44
60
  releaseMaintenance(lease: MaintenanceLease): void;
45
61
  delete(sessionId: string, lease: MaintenanceLease): void;
46
62
  prune(maxAgeDays: number, holderId: string): number;