@wrongstack/core 0.313.0 → 0.313.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34324
  131. package/dist/coordination/mailbox-project-server.js +48 -3276
  132. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  133. package/dist/core/agent.d.ts +14 -1
  134. package/dist/core/index.js +267 -12480
  135. package/dist/core/model-ref.js +5 -23
  136. package/dist/core/provider-runner.d.ts +36 -1
  137. package/dist/design/index.js +50 -1087
  138. package/dist/execution/index.d.ts +1 -1
  139. package/dist/execution/index.js +1599 -21888
  140. package/dist/execution/prompt-enhancer.js +16 -535
  141. package/dist/execution/retry-policy.d.ts +31 -1
  142. package/dist/extension/index.js +7 -311
  143. package/dist/goal/index.js +33 -3435
  144. package/dist/hooks/index.js +14 -1138
  145. package/dist/hq/index.js +112 -5139
  146. package/dist/hq/protocol.js +21 -668
  147. package/dist/index.js +1410 -92656
  148. package/dist/infrastructure/index.js +64 -4811
  149. package/dist/kernel/events/provider-events.d.ts +15 -0
  150. package/dist/kernel/index.js +14 -992
  151. package/dist/models/index.js +40 -1117
  152. package/dist/notifications/index.js +3 -108
  153. package/dist/observability/index.js +17 -279
  154. package/dist/plugin/index.js +584 -14835
  155. package/dist/prompts/index.js +14 -948
  156. package/dist/registry/index.js +9 -822
  157. package/dist/registry-R23UBE7H.js +14 -0
  158. package/dist/replay/index.js +6 -84
  159. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  160. package/dist/review-report-store-OWY3EC3N.js +19 -0
  161. package/dist/security/index.js +114 -2202
  162. package/dist/session-catalog/index.js +36 -2103
  163. package/dist/session-catalog/project-server.js +42 -1435
  164. package/dist/skills/index.js +35 -1578
  165. package/dist/storage/index.js +182 -15600
  166. package/dist/tasking/index.js +10 -676
  167. package/dist/tools/index.js +52 -9590
  168. package/dist/types/config/autonomy.d.ts +5 -0
  169. package/dist/types/config/ui.d.ts +1 -1
  170. package/dist/types/errors.d.ts +18 -0
  171. package/dist/types/index.js +121 -1342
  172. package/dist/types/provider-runner.d.ts +8 -0
  173. package/dist/types/session-markers.js +8 -144
  174. package/dist/utils/error.js +3 -4
  175. package/dist/utils/expect-defined.js +3 -9
  176. package/dist/utils/heap-watchdog.js +6 -706
  177. package/dist/utils/index.js +269 -5685
  178. package/dist/utils/sage-output-block.js +5 -42
  179. package/dist/wiring/proxy-rewrite.d.ts +18 -0
  180. package/dist/wiring/proxy-rewrite.js +18 -121
  181. package/dist/worktree/index.js +6 -887
  182. package/package.json +4 -4
@@ -1,2108 +1,41 @@
1
- // src/session-catalog/client.ts
2
- import { spawn } from "node:child_process";
3
- import * as fs2 from "node:fs";
4
- import * as net from "node:net";
5
- import * as path2 from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- // src/session-catalog/endpoint.ts
9
- import { createHash } from "node:crypto";
10
- import * as fs from "node:fs";
11
- import * as os from "node:os";
12
- import * as path from "node:path";
13
-
14
- // src/utils/socket-path.ts
1
+ import "../chunk-M7BS2K6U.js";
15
2
  import {
16
- assertUnixSocketPathWithinLimit,
17
- checkUnixSocketPath,
18
- unixSocketPathLimit
19
- } from "@wrongstack/persistence";
20
-
21
- // src/session-catalog/protocol.ts
22
- var SESSION_CATALOG_PROTOCOL_VERSION = 1;
23
- var SESSION_CATALOG_MAX_FRAME_CHARS = 4 * 1024 * 1024;
24
- var SESSION_CATALOG_DEFAULT_LEASE_MS = 3e4;
25
- var SESSION_CATALOG_DEFAULT_RESERVATION_MS = 15e3;
26
- var SESSION_CATALOG_MAX_AGENTS = 128;
27
- function encodeSessionCatalogMessage(message) {
28
- return `${JSON.stringify(message)}
29
- `;
30
- }
31
-
32
- // src/session-catalog/endpoint.ts
33
- var SESSION_CATALOG_METADATA_FILE = ".session-catalog-server.json";
34
- function normalizedPath(value) {
35
- const resolved = path.resolve(value);
36
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
37
- }
38
- function sessionCatalogProjectServerKey(projectDir) {
39
- return createHash("sha256").update(normalizedPath(projectDir)).digest("hex").slice(0, 24);
40
- }
41
- function sessionCatalogProjectServerEndpoint(projectDir) {
42
- const key = sessionCatalogProjectServerKey(projectDir);
43
- if (process.platform === "win32") {
44
- return `\\\\.\\pipe\\wrongstack-session-catalog-v${SESSION_CATALOG_PROTOCOL_VERSION}-${key}`;
45
- }
46
- return path.join(os.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
47
- }
48
- function sessionCatalogProjectServerMetadataPath(projectDir) {
49
- return path.join(projectDir, SESSION_CATALOG_METADATA_FILE);
50
- }
51
- function ensureSessionCatalogSocketDirectory(endpoint) {
52
- if (process.platform === "win32") return;
53
- assertUnixSocketPathWithinLimit(endpoint, "session-catalog");
54
- fs.mkdirSync(path.dirname(endpoint), { recursive: true, mode: 448 });
55
- }
56
-
57
- // src/session-catalog/client.ts
58
- var CONNECT_TIMEOUT_MS = 750;
59
- var START_TIMEOUT_MS = 1e4;
60
- var CALL_TIMEOUT_MS = 3e4;
61
- var MAX_PENDING_REQUESTS = 1024;
62
- var MAX_EVENT_LISTENERS = 64;
63
- function locateServer(moduleUrl, exists) {
64
- for (const candidate of [
65
- "./project-server.js",
66
- "../session-catalog/project-server.js",
67
- "./session-catalog/project-server.js",
68
- "../../dist/session-catalog/project-server.js"
69
- ]) {
70
- try {
71
- const url = new URL(candidate, moduleUrl);
72
- if (url.protocol === "file:" && exists(fileURLToPath(url))) return url;
73
- } catch {
74
- }
75
- }
76
- return null;
77
- }
78
- function resolveSessionCatalogDaemonAvailability(moduleUrl = import.meta.url, exists = fs2.existsSync) {
79
- if (process.env["WRONGSTACK_SESSION_CATALOG_INLINE"] || process.env["WRONGSTACK_SESSION_CATALOG_SERVER"] === "0")
80
- return { kind: "inline-requested" };
81
- const url = locateServer(moduleUrl, exists);
82
- return url ? { kind: "available", url } : { kind: "missing-build" };
83
- }
84
- function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exists = fs2.existsSync) {
85
- const availability = resolveSessionCatalogDaemonAvailability(moduleUrl, exists);
86
- return availability.kind === "available" ? availability.url : null;
87
- }
88
- function normalize(value) {
89
- const resolved = path2.resolve(value);
90
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
91
- }
92
- function delay(ms) {
93
- return new Promise((resolve5) => setTimeout(resolve5, ms));
94
- }
95
- var SessionCatalogProjectClient = class {
96
- constructor(options) {
97
- this.options = options;
98
- this.endpoint = sessionCatalogProjectServerEndpoint(options.projectDir);
99
- }
100
- options;
101
- endpoint;
102
- socket = null;
103
- info = null;
104
- buffer = "";
105
- connecting = null;
106
- connectResolve = null;
107
- connectReject = null;
108
- authToken;
109
- nextId = 1;
110
- pending = /* @__PURE__ */ new Map();
111
- eventListeners = /* @__PURE__ */ new Set();
112
- reconnectTimer;
113
- explicitlyClosed = false;
114
- async call(op, args, options = {}) {
115
- await this.ensureConnected(true);
116
- return this.request({ type: "request", op, args }, options.timeoutMs ?? CALL_TIMEOUT_MS);
117
- }
118
- /**
119
- * Call an already-running project daemon without starting one.
120
- *
121
- * Cross-project discovery must use this path: observing another project is
122
- * never sufficient authority to wake that project's IPC owner.
123
- */
124
- async callExisting(op, args, options = {}) {
125
- await this.ensureConnected(false);
126
- return this.request({ type: "request", op, args }, options.timeoutMs ?? CALL_TIMEOUT_MS);
127
- }
128
- ping() {
129
- return this.call("ping", {}, { timeoutMs: 3e3 });
130
- }
131
- async subscribe(listener) {
132
- if (!this.eventListeners.has(listener) && this.eventListeners.size >= MAX_EVENT_LISTENERS) {
133
- throw new Error(`Session Catalog listener limit reached (${MAX_EVENT_LISTENERS})`);
134
- }
135
- this.explicitlyClosed = false;
136
- this.eventListeners.add(listener);
137
- try {
138
- await this.call("subscribe", {});
139
- } catch (error) {
140
- this.eventListeners.delete(listener);
141
- throw error;
142
- }
143
- return async () => {
144
- this.eventListeners.delete(listener);
145
- if (this.eventListeners.size === 0)
146
- await this.callExisting("unsubscribe", {}).catch(() => void 0);
147
- };
148
- }
149
- async shutdown(reason) {
150
- try {
151
- await this.ensureConnected(false);
152
- } catch {
153
- return { stopped: false };
154
- }
155
- const result = await this.request({ type: "shutdown", ...reason !== void 0 ? { reason } : {} }, 5e3).catch(
156
- () => ({ stopped: false })
157
- );
158
- if (result.stopped) {
159
- const metadataPath = sessionCatalogProjectServerMetadataPath(this.options.projectDir);
160
- const deadline = Date.now() + 5e3;
161
- while (fs2.existsSync(metadataPath) && Date.now() < deadline) await delay(20);
162
- if (result.pid && result.pid !== process.pid) {
163
- while (Date.now() < deadline) {
164
- try {
165
- process.kill(result.pid, 0);
166
- await delay(20);
167
- } catch {
168
- break;
169
- }
170
- }
171
- }
172
- }
173
- return result;
174
- }
175
- async close() {
176
- this.explicitlyClosed = true;
177
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
178
- this.reconnectTimer = void 0;
179
- const socket = this.socket;
180
- this.socket = null;
181
- this.info = null;
182
- if (socket && !socket.destroyed)
183
- await new Promise((resolve5) => {
184
- socket.once("close", resolve5);
185
- socket.end();
186
- });
187
- }
188
- async ensureConnected(spawnIfMissing) {
189
- if (this.socket && !this.socket.destroyed && this.info) return;
190
- if (this.connecting) return this.connecting;
191
- this.connecting = this.connectWithElection(spawnIfMissing).finally(() => {
192
- this.connecting = null;
193
- });
194
- return this.connecting;
195
- }
196
- async connectWithElection(spawnIfMissing) {
197
- const deadline = Date.now() + (spawnIfMissing ? START_TIMEOUT_MS : CONNECT_TIMEOUT_MS);
198
- let spawned = false;
199
- let lastError = new Error("Session Catalog project server unavailable");
200
- while (Date.now() < deadline) {
201
- try {
202
- await this.connectOnce();
203
- return;
204
- } catch (error) {
205
- lastError = error;
206
- }
207
- if (!spawnIfMissing) break;
208
- if (!spawned) {
209
- this.spawnDetached();
210
- spawned = true;
211
- }
212
- await delay(75);
213
- }
214
- throw lastError;
215
- }
216
- connectOnce() {
217
- this.socket?.destroy();
218
- this.socket = null;
219
- this.info = null;
220
- this.authToken = void 0;
221
- this.buffer = "";
222
- return new Promise((resolve5, reject) => {
223
- const socket = net.createConnection(this.endpoint);
224
- this.socket = socket;
225
- socket.setEncoding("utf8");
226
- const timer = setTimeout(() => {
227
- reject(new Error("Session Catalog handshake timed out"));
228
- socket.destroy();
229
- }, CONNECT_TIMEOUT_MS);
230
- timer.unref?.();
231
- this.connectResolve = () => {
232
- clearTimeout(timer);
233
- this.connectResolve = null;
234
- this.connectReject = null;
235
- resolve5();
236
- };
237
- this.connectReject = (error) => {
238
- clearTimeout(timer);
239
- this.connectResolve = null;
240
- this.connectReject = null;
241
- reject(error);
242
- };
243
- socket.on("data", (chunk) => this.onData(socket, chunk));
244
- socket.on("error", (error) => {
245
- if (!this.info) this.connectReject?.(error);
246
- });
247
- socket.on("close", () => this.onClose(socket));
248
- });
249
- }
250
- currentAuthToken() {
251
- if (this.authToken === void 0) {
252
- try {
253
- const parsed = JSON.parse(
254
- fs2.readFileSync(sessionCatalogProjectServerMetadataPath(this.options.projectDir), "utf8")
255
- );
256
- if (typeof parsed.authToken === "string" && parsed.authToken)
257
- this.authToken = parsed.authToken;
258
- } catch {
259
- }
260
- }
261
- return this.authToken;
262
- }
263
- request(message, timeoutMs) {
264
- const socket = this.socket;
265
- if (!socket || socket.destroyed)
266
- return Promise.reject(new Error("Session Catalog connection is unavailable"));
267
- const id = this.nextId++;
268
- if (this.pending.size >= MAX_PENDING_REQUESTS) {
269
- return Promise.reject(
270
- new Error(`Session Catalog pending request limit reached (${MAX_PENDING_REQUESTS})`)
271
- );
272
- }
273
- const encoded = encodeSessionCatalogMessage({
274
- ...message,
275
- id,
276
- authToken: this.currentAuthToken()
277
- });
278
- if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
279
- return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
280
- return new Promise((resolve5, reject) => {
281
- const timer = setTimeout(() => {
282
- const pending = this.pending.get(id);
283
- if (!pending) return;
284
- this.pending.delete(id);
285
- pending.reject(
286
- new Error(
287
- `Session Catalog ${message.type === "request" ? message.op : message.type} timed out`
288
- )
289
- );
290
- }, timeoutMs);
291
- timer.unref?.();
292
- this.pending.set(id, { resolve: resolve5, reject, timer });
293
- socket.write(encoded);
294
- });
295
- }
296
- onData(socket, chunk) {
297
- if (socket !== this.socket) return;
298
- this.buffer += chunk;
299
- if (this.buffer.length > SESSION_CATALOG_MAX_FRAME_CHARS) {
300
- socket.destroy(new Error("Session Catalog response exceeded frame limit"));
301
- return;
302
- }
303
- while (true) {
304
- const newline = this.buffer.indexOf("\n");
305
- if (newline < 0) return;
306
- const line = this.buffer.slice(0, newline);
307
- this.buffer = this.buffer.slice(newline + 1);
308
- if (!line) continue;
309
- try {
310
- this.onMessage(JSON.parse(line));
311
- } catch {
312
- socket.destroy(new Error("Invalid Session Catalog response"));
313
- return;
314
- }
315
- }
316
- }
317
- onMessage(message) {
318
- if (message.type === "hello") {
319
- if (message.protocolVersion !== SESSION_CATALOG_PROTOCOL_VERSION) {
320
- this.connectReject?.(
321
- new Error(
322
- `Session Catalog protocol mismatch: client=${SESSION_CATALOG_PROTOCOL_VERSION}, server=${message.protocolVersion}`
323
- )
324
- );
325
- this.socket?.destroy();
326
- return;
327
- }
328
- if (normalize(message.projectDir) !== normalize(this.options.projectDir) || normalize(message.projectRoot) !== normalize(this.options.projectRoot)) {
329
- this.connectReject?.(new Error("Session Catalog project identity mismatch"));
330
- this.socket?.destroy();
331
- return;
332
- }
333
- this.info = message;
334
- this.connectResolve?.();
335
- return;
336
- }
337
- if (message.type === "event") {
338
- for (const listener of this.eventListeners) {
339
- try {
340
- listener(message.event);
341
- } catch {
342
- }
343
- }
344
- return;
345
- }
346
- const pending = this.pending.get(message.id);
347
- if (!pending) return;
348
- this.pending.delete(message.id);
349
- clearTimeout(pending.timer);
350
- if (message.ok) pending.resolve(message.result);
351
- else {
352
- const error = new Error(message.error);
353
- if (message.errorName) error.name = message.errorName;
354
- pending.reject(error);
355
- }
356
- }
357
- onClose(socket) {
358
- if (socket !== this.socket) return;
359
- this.socket = null;
360
- this.info = null;
361
- this.authToken = void 0;
362
- const error = new Error("Session Catalog connection closed");
363
- this.connectReject?.(error);
364
- this.connectResolve = null;
365
- this.connectReject = null;
366
- for (const pending of this.pending.values()) {
367
- clearTimeout(pending.timer);
368
- pending.reject(error);
369
- }
370
- this.pending.clear();
371
- this.scheduleSubscriptionReconnect();
372
- }
373
- scheduleSubscriptionReconnect() {
374
- if (this.explicitlyClosed || this.eventListeners.size === 0 || this.reconnectTimer) return;
375
- this.reconnectTimer = setTimeout(() => {
376
- this.reconnectTimer = void 0;
377
- void this.call("subscribe", {}).catch(() => this.scheduleSubscriptionReconnect());
378
- }, 250);
379
- this.reconnectTimer.unref?.();
380
- }
381
- spawnDetached() {
382
- const url = resolveSessionCatalogProjectServerUrl();
383
- if (!url) throw new Error("Built Session Catalog project server is unavailable");
384
- const child = spawn(
385
- process.execPath,
386
- [
387
- fileURLToPath(url),
388
- "--project-dir",
389
- this.options.projectDir,
390
- "--project-root",
391
- this.options.projectRoot
392
- ],
393
- { detached: true, stdio: "ignore", windowsHide: true, env: process.env }
394
- );
395
- child.unref();
396
- }
397
- };
398
-
399
- // src/session-catalog/registry.ts
400
- import { randomUUID } from "node:crypto";
401
- import * as fs3 from "node:fs/promises";
402
- import * as path3 from "node:path";
403
- var HEARTBEAT_INTERVAL_MS = 5e3;
404
- var AGENT_WRITE_THROTTLE_MS = 300;
405
- var MAX_PROJECT_CLIENTS = 128;
406
- var MAX_GLOBAL_ROOTS = 16;
407
- var ProjectSessionRegistry = class {
408
- constructor(globalRoot) {
409
- this.globalRoot = globalRoot;
410
- }
411
- globalRoot;
412
- instanceId = randomUUID();
413
- clients = /* @__PURE__ */ new Map();
414
- current;
415
- heartbeatTimer;
416
- agentRevision = 0;
417
- pendingAgents;
418
- agentTimer;
419
- lastAgentWriteAt = 0;
420
- bindingKey(projectDir) {
421
- const resolved = path3.resolve(projectDir);
422
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
423
- }
424
- async closeBinding(binding) {
425
- if (this.current?.binding === binding) return;
426
- const key = this.bindingKey(binding.projectDir);
427
- if (this.clients.get(key) === binding) this.clients.delete(key);
428
- await binding.client.close();
429
- }
430
- binding(projectSlug, projectRoot) {
431
- const projectDir = path3.join(this.globalRoot, "projects", projectSlug);
432
- const key = this.bindingKey(projectDir);
433
- let binding = this.clients.get(key);
434
- if (!binding) {
435
- if (this.clients.size >= MAX_PROJECT_CLIENTS) {
436
- const oldest = [...this.clients.entries()].find(
437
- ([, candidate]) => candidate !== this.current?.binding
438
- );
439
- if (oldest) {
440
- this.clients.delete(oldest[0]);
441
- void oldest[1].client.close();
442
- }
443
- }
444
- binding = {
445
- projectDir,
446
- projectRoot: path3.resolve(projectRoot),
447
- client: new SessionCatalogProjectClient({ projectDir, projectRoot })
448
- };
449
- this.clients.set(key, binding);
450
- } else {
451
- this.clients.delete(key);
452
- this.clients.set(key, binding);
453
- }
454
- return binding;
455
- }
456
- fullEntry(entry) {
457
- const agents = entry.agents ?? [];
458
- return {
459
- ...entry,
460
- status: agents.some((agent) => agent.status !== "idle") ? "active" : "idle",
461
- lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
462
- agentCount: agents.length,
463
- agents
464
- };
465
- }
466
- async register(entry) {
467
- const full = this.fullEntry(entry);
468
- if (this.current?.entry.sessionId === full.sessionId && this.current.entry.pid === full.pid) {
469
- this.current.entry = full;
470
- this.current.credential = await this.current.binding.client.call("heartbeat", {
471
- credential: this.current.credential,
472
- status: full.status
473
- });
474
- return;
475
- }
476
- const nextBinding = this.binding(full.projectSlug, full.projectRoot);
477
- let nextCredential;
478
- try {
479
- nextCredential = await nextBinding.client.call("claim_new", {
480
- entry: full,
481
- ownerInstanceId: this.instanceId
482
- });
483
- } catch (error) {
484
- await this.closeBinding(nextBinding).catch(() => void 0);
485
- throw error;
486
- }
487
- const previous = this.current;
488
- this.current = { binding: nextBinding, credential: nextCredential, entry: full };
489
- this.agentRevision = 0;
490
- this.cancelAgentTimer();
491
- this.startHeartbeat();
492
- if (previous) {
493
- await previous.binding.client.call("release", { credential: previous.credential }).catch(() => void 0);
494
- if (previous.binding !== nextBinding) await this.closeBinding(previous.binding);
495
- }
496
- }
497
- /** Reserve before transcript hydration; activation swaps ownership only after the writer opened. */
498
- async reserveResume(target) {
499
- const binding = this.binding(target.projectSlug, target.projectRoot);
500
- let reservation;
501
- try {
502
- reservation = await binding.client.call("reserve_resume", {
503
- targetSessionId: target.sessionId,
504
- requesterInstanceId: this.instanceId,
505
- ...this.current ? { currentSessionId: this.current.entry.sessionId } : {}
506
- });
507
- } catch (error) {
508
- await this.closeBinding(binding).catch(() => void 0);
509
- throw error;
510
- }
511
- let settled = false;
512
- return {
513
- reservation,
514
- activate: async (registration) => {
515
- if (settled) throw new Error("Resume reservation is already settled");
516
- const entry = this.fullEntry(registration);
517
- const credential = await binding.client.call("activate_reservation", {
518
- reservation,
519
- entry
520
- });
521
- const previous = this.current;
522
- this.current = { binding, credential, entry };
523
- settled = true;
524
- this.agentRevision = 0;
525
- this.cancelAgentTimer();
526
- this.startHeartbeat();
527
- if (previous) {
528
- await previous.binding.client.call("release", { credential: previous.credential }).catch(() => void 0);
529
- if (previous.binding !== binding) await this.closeBinding(previous.binding);
530
- }
531
- },
532
- cancel: async () => {
533
- if (settled) return;
534
- settled = true;
535
- try {
536
- await binding.client.call("cancel_reservation", {
537
- reservationId: reservation.reservationId,
538
- requesterInstanceId: this.instanceId
539
- }).catch(() => void 0);
540
- } finally {
541
- await this.closeBinding(binding).catch(() => void 0);
542
- }
543
- }
544
- };
545
- }
546
- async updateAgents(agents) {
547
- if (!this.current) return;
548
- this.pendingAgents = agents;
549
- this.current.entry = {
550
- ...this.current.entry,
551
- agents,
552
- agentCount: agents.length,
553
- status: agents.some((agent) => agent.status !== "idle") ? "active" : "idle",
554
- lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
555
- };
556
- const elapsed = Date.now() - this.lastAgentWriteAt;
557
- if (!this.agentTimer && elapsed >= AGENT_WRITE_THROTTLE_MS) {
558
- await this.flushAgents();
559
- return;
560
- }
561
- if (!this.agentTimer) {
562
- this.agentTimer = setTimeout(
563
- () => {
564
- this.agentTimer = void 0;
565
- void this.flushAgents();
566
- },
567
- Math.max(0, AGENT_WRITE_THROTTLE_MS - elapsed)
568
- );
569
- this.agentTimer.unref?.();
570
- }
571
- }
572
- async flushAgents() {
573
- const agents = this.pendingAgents;
574
- const current = this.current;
575
- if (!agents || !current) return;
576
- this.pendingAgents = void 0;
577
- this.lastAgentWriteAt = Date.now();
578
- const revision = ++this.agentRevision;
579
- await current.binding.client.call("publish_agents", {
580
- credential: current.credential,
581
- revision,
582
- agents
583
- });
584
- }
585
- async markClosing() {
586
- this.stopHeartbeat();
587
- this.cancelAgentTimer();
588
- if (this.current)
589
- await this.current.binding.client.call("mark_closing", {
590
- credential: this.current.credential
591
- });
592
- }
593
- async unregister() {
594
- this.stopHeartbeat();
595
- this.cancelAgentTimer();
596
- const current = this.current;
597
- this.current = void 0;
598
- if (current) {
599
- try {
600
- await current.binding.client.call("release", { credential: current.credential });
601
- } finally {
602
- await this.closeBinding(current.binding);
603
- }
604
- }
605
- }
606
- async list() {
607
- const projectsDir = path3.join(this.globalRoot, "projects");
608
- let directories = [];
609
- try {
610
- directories = (await fs3.readdir(projectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).slice(0, 1e3).map((entry) => entry.name);
611
- } catch {
612
- return [];
613
- }
614
- const snapshots = await Promise.all(
615
- directories.map(async (slug) => {
616
- const projectDir = path3.join(projectsDir, slug);
617
- let client;
618
- try {
619
- const metadata = JSON.parse(
620
- await fs3.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
621
- );
622
- if (typeof metadata.projectRoot !== "string" || !metadata.projectRoot) return [];
623
- client = new SessionCatalogProjectClient({
624
- projectDir,
625
- projectRoot: metadata.projectRoot
626
- });
627
- return await client.callExisting("list_live", {});
628
- } catch {
629
- return [];
630
- } finally {
631
- await client?.close().catch(() => void 0);
632
- }
633
- })
634
- );
635
- return snapshots.flat();
636
- }
637
- async listByProject(projectSlug) {
638
- const current = this.current;
639
- if (current && path3.basename(current.binding.projectDir) === projectSlug) {
640
- return current.binding.client.call("list_live", {}).catch(() => []);
641
- }
642
- const projectDir = path3.join(this.globalRoot, "projects", projectSlug);
643
- let client;
644
- try {
645
- const metadata = JSON.parse(
646
- await fs3.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
647
- );
648
- if (typeof metadata.projectRoot !== "string") return [];
649
- client = new SessionCatalogProjectClient({ projectDir, projectRoot: metadata.projectRoot });
650
- return await client.callExisting("list_live", {}).catch(() => []);
651
- } catch {
652
- return [];
653
- } finally {
654
- await client?.close().catch(() => void 0);
655
- }
656
- }
657
- async get(sessionId) {
658
- return (await this.list()).find((entry) => entry.sessionId === sessionId);
659
- }
660
- subscribeProject(projectSlug, projectRoot, listener) {
661
- const binding = this.binding(projectSlug, projectRoot);
662
- return binding.client.subscribe(listener).then((unsubscribe) => async () => {
663
- await unsubscribe();
664
- await this.closeBinding(binding);
665
- });
666
- }
667
- get registryPath() {
668
- return path3.join(this.globalRoot, "projects");
669
- }
670
- async dispose() {
671
- await this.unregister().catch(() => void 0);
672
- await Promise.all([...this.clients.values()].map((binding) => binding.client.close()));
673
- this.clients.clear();
674
- }
675
- /** Whether this facade currently owns a live session lease. */
676
- ownsSession() {
677
- return this.current !== void 0;
678
- }
679
- startHeartbeat() {
680
- this.stopHeartbeat();
681
- this.heartbeatTimer = setInterval(() => {
682
- void this.heartbeat();
683
- }, HEARTBEAT_INTERVAL_MS);
684
- this.heartbeatTimer.unref?.();
685
- }
686
- stopHeartbeat() {
687
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
688
- this.heartbeatTimer = void 0;
689
- }
690
- cancelAgentTimer() {
691
- if (this.agentTimer) clearTimeout(this.agentTimer);
692
- this.agentTimer = void 0;
693
- this.pendingAgents = void 0;
694
- }
695
- async heartbeat() {
696
- const current = this.current;
697
- if (!current) return;
698
- try {
699
- const credential = await current.binding.client.call("heartbeat", {
700
- credential: current.credential,
701
- status: current.entry.status
702
- });
703
- if (this.current === current) current.credential = credential;
704
- } catch {
705
- }
706
- }
707
- };
708
- var registries = /* @__PURE__ */ new Map();
709
- var lastRegistryKey;
710
- function getProjectSessionRegistry(globalRoot) {
711
- const key = globalRoot !== void 0 ? path3.resolve(globalRoot) : lastRegistryKey;
712
- if (!key)
713
- throw new Error("SessionRegistry not initialized. Call getSessionRegistry(globalRoot) first.");
714
- let registry = registries.get(key);
715
- if (!registry) {
716
- if (registries.size >= MAX_GLOBAL_ROOTS) {
717
- const idle = [...registries.entries()].find(([, candidate]) => !candidate.ownsSession());
718
- if (!idle)
719
- throw new Error(`Session Registry global-root limit reached (${MAX_GLOBAL_ROOTS})`);
720
- registries.delete(idle[0]);
721
- void idle[1].dispose();
722
- }
723
- registry = new ProjectSessionRegistry(key);
724
- registries.set(key, registry);
725
- } else {
726
- registries.delete(key);
727
- registries.set(key, registry);
728
- }
729
- lastRegistryKey = key;
730
- return registry;
731
- }
732
- function hasProjectSessionRegistry(globalRoot) {
733
- if (globalRoot === void 0) return registries.size > 0;
734
- return registries.has(path3.resolve(globalRoot));
735
- }
736
-
737
- // src/session-catalog/store.ts
738
- import { randomBytes, randomUUID as randomUUID2 } from "node:crypto";
739
- import * as fs5 from "node:fs";
740
- import * as path5 from "node:path";
741
-
742
- // src/coordination/sqlite-mailbox-schema.ts
743
- import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
744
-
745
- // src/utils/sqlite-warning.ts
746
- var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
747
- function isSqliteExperimentalWarning(warning, rest) {
748
- const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
749
- const typeOrOptions = rest[0];
750
- const warningType = typeof warning === "string" ? typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type : "" : warning instanceof Error ? warning.name : "";
751
- const warningCode = typeof warning === "string" ? typeof typeOrOptions === "object" && typeOrOptions !== null && "code" in typeOrOptions && typeof typeOrOptions.code === "string" ? typeOrOptions.code : typeof rest[1] === "string" ? rest[1] : "" : warning instanceof Error && "code" in warning && typeof warning.code === "string" ? warning.code : "";
752
- return SQLITE_EXPERIMENTAL_WARNING_RE.test(message) && (warningType === "ExperimentalWarning" || warningCode === "ExperimentalWarning");
753
- }
754
- function withSqliteExperimentalWarningSuppressed(run) {
755
- const originalEmitWarning = process.emitWarning;
756
- const forwardWarning = originalEmitWarning.bind(process);
757
- process.emitWarning = ((warning, ...rest) => {
758
- if (isSqliteExperimentalWarning(warning, rest)) return;
759
- forwardWarning(warning, ...rest);
760
- });
761
- try {
762
- return run();
763
- } finally {
764
- process.emitWarning = originalEmitWarning;
765
- }
766
- }
767
-
768
- // src/coordination/sqlite-mailbox-schema.ts
769
- var DatabaseSyncCtor;
770
- function loadDatabaseSync() {
771
- if (DatabaseSyncCtor) return DatabaseSyncCtor;
772
- return withSqliteExperimentalWarningSuppressed(() => {
773
- DatabaseSyncCtor = loadRuntimeDatabaseSync();
774
- return DatabaseSyncCtor;
775
- });
776
- }
777
-
778
- // src/security/secret-scrubber.ts
779
- var JSON_CREDENTIAL_KEY_ANCHORS = [
780
- 'Key"',
781
- 'key"',
782
- 'KEY"',
783
- 'token"',
784
- 'Token"',
785
- 'TOKEN"',
786
- 'secret"',
787
- 'Secret"',
788
- 'SECRET"',
789
- 'password"',
790
- 'Password"',
791
- 'PASSWORD"',
792
- 'authorization"',
793
- 'Authorization"',
794
- 'bearer"',
795
- 'Bearer"'
796
- ];
797
- var PATTERNS = [
798
- // Anchored at the start where possible so partial matches inside larger
799
- // strings don't trigger false positives.
800
- {
801
- type: "anthropic_key",
802
- regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g,
803
- anchor: "sk-ant-"
804
- },
805
- { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
806
- {
807
- // `xai` is a first-class provider in this codebase, but its key shape was
808
- // absent here — so the one credential format WrongStack itself hands users
809
- // was the one the scrubber could not recognize (audit 2026-08-20).
810
- type: "xai_key",
811
- regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
812
- anchor: "xai-"
813
- },
814
- { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
815
- { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
816
- { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
817
- { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g, anchor: "AIza" },
818
- { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g, anchor: "xox" },
819
- {
820
- type: "stripe_key",
821
- regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g,
822
- anchor: "sk_"
823
- },
824
- {
825
- type: "twilio_sid",
826
- regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g,
827
- anchor: "AC"
828
- },
829
- {
830
- type: "telegram_bot_token",
831
- // Telegram tokens are of the form bot<digits>:<alphanum> in URL paths
832
- regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
833
- anchor: "/bot"
834
- },
835
- {
836
- type: "jwt",
837
- // Anchored: look for literal "eyJ" which is unambiguous for JWT header
838
- regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g,
839
- anchor: "eyJ"
840
- },
841
- {
842
- type: "private_key",
843
- // Anchored: start must be BEGIN, end must be END with no extra dashes after END
844
- regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g,
845
- anchor: "-----BEGIN"
846
- },
847
- { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g, anchor: "mongodb" },
848
- { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g, anchor: "postgres" },
849
- { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g, anchor: "mysql://" },
850
- { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g, anchor: "redis://" },
851
- // AI/ML provider keys — modern LLM services with well-known prefixes
852
- {
853
- type: "huggingface_token",
854
- // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
855
- regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g,
856
- anchor: "hf_"
857
- },
858
- {
859
- type: "replicate_token",
860
- // Replicate tokens: r8_ followed by 40+ alphanumeric chars
861
- regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
862
- anchor: "r8_"
863
- },
864
- {
865
- type: "perplexity_key",
866
- // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
867
- regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
868
- anchor: "pplx-"
869
- },
870
- {
871
- type: "groq_key",
872
- // Groq API keys: gsk_ followed by 40+ alphanumeric chars
873
- regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
874
- anchor: "gsk_"
875
- },
876
- {
877
- type: "bearer_token",
878
- // Anchored with alternation instead of negative lookahead — avoids V8
879
- // backtracking risk on adversarial input. Bounded at 512 chars.
880
- // Min 12 chars: some OAuth providers issue shorter-lived tokens (< 20
881
- // chars). A 12-char base64 string has ~71 bits of entropy — above the
882
- // threshold where random strings are unlikely to produce false matches.
883
- // The trailing boundary is a NON-consuming lookahead: two adjacent bearer
884
- // tokens sharing a single delimiter (`Bearer a… Bearer b…`) must both be
885
- // redacted. A consuming trailing delimiter would eat the separator the
886
- // next match needs for its leading anchor, leaking the second token.
887
- regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?=$|[^A-Za-z0-9_.~+/-])/g,
888
- anchor: "Bearer"
889
- },
890
- {
891
- type: "high_entropy_env",
892
- // Anchored with alternation instead of lookbehind to avoid backtracking.
893
- // Value bounded at 512 chars.
894
- // The trailing boundary is a NON-consuming lookahead so two secrets
895
- // separated by a single delimiter (one space OR one newline, e.g.
896
- // `printenv` / `.env` dumps: `API_KEY=… \n SESSION_TOKEN=…`) are BOTH
897
- // redacted. A consuming trailing `\s` would swallow the separator the
898
- // next match needs for its leading anchor, so every other secret would
899
- // leak in plaintext.
900
- // The leading delimiter is CAPTURED (group 1) and re-emitted by the
901
- // replacement so the separator between adjacent secrets is preserved
902
- // rather than collapsed. Capture groups are therefore: 1=leading
903
- // delimiter, 2=key name, 3=value.
904
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
905
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
906
- },
907
- {
908
- type: "json_credential_key",
909
- // The JSON counterpart to `high_entropy_env`, and the pattern that the
910
- // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
911
- // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
912
- // "this text may hold a secret", every pattern then declined to match, and
913
- // the value went out verbatim. `high_entropy_env` cannot cover these: it
914
- // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
915
- // never matched.
916
- //
917
- // Tool results are routinely serialised as JSON, and a credential with no
918
- // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
919
- // has no other pattern that can catch it — this is the only thing standing
920
- // between such a value and the session JSONL, chronicle, HQ broadcast and
921
- // the model's own context.
922
- //
923
- // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
924
- // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
925
- // the closing quote has to follow the word immediately.
926
- // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
927
- // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
928
- 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,
929
- anchor: JSON_CREDENTIAL_KEY_ANCHORS
930
- },
931
- // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
932
- // The plugin runtime carried 37 patterns while this scrubber — the one that
933
- // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
934
- // audit — carried 22. The plugin side already had a parity test; it just did
935
- // not cover core. Most consequential: WrongStack mints `gho_` tokens itself
936
- // in the Copilot OAuth flow, and `gh[ousr]_` was absent here.
937
- {
938
- type: "github_oauth_token",
939
- regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g,
940
- anchor: ["gho_", "ghu_", "ghs_", "ghr_"]
941
- },
942
- {
943
- type: "gitlab_pat",
944
- regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
945
- anchor: "glpat-"
946
- },
947
- {
948
- type: "gitlab_runner_token",
949
- regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
950
- anchor: "glrt-"
951
- },
952
- {
953
- type: "npm_token",
954
- regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g,
955
- anchor: "npm_"
956
- },
957
- {
958
- type: "slack_app_token",
959
- regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g,
960
- anchor: "xapp-"
961
- },
962
- {
963
- type: "slack_webhook",
964
- regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g,
965
- anchor: "hooks.slack.com"
966
- },
967
- {
968
- type: "sendgrid_key",
969
- regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
970
- anchor: "SG."
971
- },
972
- {
973
- type: "digitalocean_token",
974
- regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g,
975
- anchor: "dop_v1_"
976
- },
977
- {
978
- type: "doppler_token",
979
- regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
980
- anchor: "dp."
981
- },
982
- {
983
- type: "shopify_token",
984
- regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g,
985
- anchor: "shp"
986
- },
987
- {
988
- type: "docker_pat",
989
- regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
990
- anchor: "dckr_pat_"
991
- },
992
- {
993
- type: "linear_key",
994
- regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g,
995
- anchor: "lin_api_"
996
- },
997
- {
998
- type: "atlassian_token",
999
- regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g,
1000
- anchor: "ATATT3"
1001
- },
1002
- {
1003
- type: "square_token",
1004
- regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1005
- anchor: ["sq0atp-", "sq0csp-", "EAAA"]
1006
- },
1007
- {
1008
- type: "google_oauth_client_secret",
1009
- regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g,
1010
- anchor: "GOCSPX-"
1011
- }
1012
- ];
1013
- var SIMPLE_PATTERNS = PATTERNS.filter(
1014
- (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
1015
- );
1016
- var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
1017
- var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
1018
- var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
1019
- var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1020
- var SCRUB_CHUNK_BYTES = 64 * 1024;
1021
- var SCRUB_OVERLAP_BYTES = 1024;
1022
- var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1023
- var PEM_END_MARKER = "-----END";
1024
- var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1025
- var PEM_END_LINE_TOLERANCE = 64;
1026
- function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1027
- const head = text.slice(chunkStart, proposedEnd);
1028
- const lastBegin = head.lastIndexOf("-----BEGIN ");
1029
- if (lastBegin === -1) return proposedEnd;
1030
- const fromBegin = text.slice(chunkStart + lastBegin);
1031
- const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1032
- if (!marker || marker.index !== 0) return proposedEnd;
1033
- const bodyStart = marker[0].length;
1034
- const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1035
- const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1036
- if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1037
- return proposedEnd;
1038
- }
1039
- const lineEnd = fromBegin.indexOf("\n", closeIdx);
1040
- const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1041
- return Math.max(proposedEnd, end);
1042
- }
1043
- var PATTERN_ANCHORS = [
1044
- ...new Set(
1045
- PATTERNS.flatMap(
1046
- (pattern) => typeof pattern.anchor === "string" ? [pattern.anchor] : [...pattern.anchor]
1047
- )
1048
- )
1049
- ];
1050
- var ALL_ANCHORS = PATTERN_ANCHORS;
1051
- function hasCredentialAnchors(text) {
1052
- for (const anchor of ALL_ANCHORS) {
1053
- if (text.includes(anchor)) return true;
1054
- }
1055
- return false;
1056
- }
1057
- var DefaultSecretScrubber = class {
1058
- scrub(text) {
1059
- if (!text) return text;
1060
- if (!hasCredentialAnchors(text)) return text;
1061
- if (text.length <= SCRUB_CHUNK_BYTES) {
1062
- return this.scrubOne(text);
1063
- }
1064
- const out = [];
1065
- let i = 0;
1066
- while (i < text.length) {
1067
- let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
1068
- if (end < text.length) {
1069
- const limit = Math.min(end + SCRUB_OVERLAP_BYTES, text.length);
1070
- let safe = -1;
1071
- for (let j = end; j < limit; j++) {
1072
- const ch = text.charCodeAt(j);
1073
- if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
1074
- safe = j;
1075
- break;
1076
- }
1077
- }
1078
- end = safe === -1 ? end : safe + 1;
1079
- end = extendChunkBoundaryPastPem(text, i, end);
1080
- }
1081
- out.push(this.scrubOne(text.slice(i, end)));
1082
- i = end;
1083
- }
1084
- return out.join("");
1085
- }
1086
- scrubOne(text) {
1087
- if (!hasCredentialAnchors(text)) return text;
1088
- let out = text.replace(
1089
- COMBINED_REGEX,
1090
- (match, ...groups) => {
1091
- const idx = groups.findIndex((g) => g !== void 0);
1092
- if (idx < 0) return match;
1093
- const replacement = COMBINED_REPLACEMENTS[idx];
1094
- return replacement !== void 0 ? replacement : match;
1095
- }
1096
- );
1097
- out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
1098
- return `${lead}${key}=[REDACTED:high_entropy_env]`;
1099
- });
1100
- out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
1101
- return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
1102
- });
1103
- return out;
1104
- }
1105
- /**
1106
- * Recursively scrub every string value in an object/array graph. Secrets can
1107
- * appear under any key — a URL query param, an `authorization` header, an
1108
- * arbitrarily-named nested field — so we don't gate recursion on key names.
1109
- * The per-string `scrub()` fast-path (anchor pre-scan) keeps this cheap: any
1110
- * value without a credential anchor returns immediately without regex work.
1111
- */
1112
- scrubObject(obj) {
1113
- const seen = /* @__PURE__ */ new WeakSet();
1114
- const visit = (v) => {
1115
- if (typeof v === "string") return this.scrub(v);
1116
- if (v === null || typeof v !== "object") return v;
1117
- if (seen.has(v)) return v;
1118
- seen.add(v);
1119
- if (Array.isArray(v)) return v.map(visit);
1120
- const out = {};
1121
- for (const [k, val] of Object.entries(v)) {
1122
- out[k] = visit(val);
1123
- }
1124
- return out;
1125
- };
1126
- return visit(obj);
1127
- }
1128
- };
1129
-
1130
- // src/utils/atomic-write.ts
3
+ ProjectSessionRegistry,
4
+ getProjectSessionRegistry,
5
+ hasProjectSessionRegistry
6
+ } from "../chunk-YBUJCZ57.js";
1131
7
  import {
1132
- createPersistencePrimitives
1133
- } from "@wrongstack/persistence";
1134
-
1135
- // src/types/errors.ts
1136
- var ERROR_CODES = {
1137
- // Provider
1138
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
1139
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
1140
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
1141
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
1142
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
1143
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
1144
- PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
1145
- // Tool
1146
- TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
1147
- TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
1148
- TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
1149
- TOOL_TIMEOUT: "TOOL_TIMEOUT",
1150
- TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
1151
- // Config
1152
- CONFIG_INVALID: "CONFIG_INVALID",
1153
- CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
1154
- CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
1155
- CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
1156
- // Plugin
1157
- PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
1158
- PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
1159
- PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
1160
- // Agent
1161
- AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
1162
- AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
1163
- AGENT_ABORTED: "AGENT_ABORTED",
1164
- AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
1165
- // Session
1166
- SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
1167
- SESSION_CORRUPTED: "SESSION_CORRUPTED",
1168
- SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
1169
- // Container / Registry
1170
- CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
1171
- CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
1172
- CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
1173
- REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
1174
- REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
1175
- REGISTRY_INVALID: "REGISTRY_INVALID",
1176
- // File system
1177
- FS_READ_FAILED: "FS_READ_FAILED",
1178
- FS_WRITE_FAILED: "FS_WRITE_FAILED",
1179
- FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
1180
- FS_DELETE_FAILED: "FS_DELETE_FAILED",
1181
- FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
1182
- // SDD (Spec-Driven Development)
1183
- SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
1184
- SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
1185
- SDD_INVALID_STATE: "SDD_INVALID_STATE",
1186
- SDD_NOT_READY: "SDD_NOT_READY",
1187
- // General
1188
- VALIDATION_ERROR: "VALIDATION_ERROR",
1189
- PARSE_FAILED: "PARSE_FAILED",
1190
- UNKNOWN: "UNKNOWN"
1191
- };
1192
- var WrongStackError = class extends Error {
1193
- code;
1194
- subsystem;
1195
- severity;
1196
- recoverable;
1197
- context;
1198
- constructor(opts) {
1199
- super(opts.message, { cause: opts.cause });
1200
- this.name = "WrongStackError";
1201
- this.code = opts.code;
1202
- this.subsystem = opts.subsystem;
1203
- this.severity = opts.severity ?? "error";
1204
- this.recoverable = opts.recoverable ?? false;
1205
- this.context = opts.context;
1206
- }
1207
- /**
1208
- * Render a one-line user-facing description.
1209
- * Subclasses should override for domain-specific formatting.
1210
- */
1211
- describe() {
1212
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
1213
- return `${this.code}: ${this.message}${ctx}`;
1214
- }
1215
- };
1216
- function formatContext(ctx) {
1217
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
1218
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
1219
- }
1220
- var FsError = class extends WrongStackError {
1221
- path;
1222
- constructor(opts) {
1223
- super({
1224
- message: opts.message,
1225
- code: opts.code,
1226
- subsystem: "fs",
1227
- severity: "error",
1228
- recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
1229
- context: { path: opts.path, ...opts.context },
1230
- cause: opts.cause
1231
- });
1232
- this.name = "FsError";
1233
- this.path = opts.path;
1234
- }
1235
- };
1236
-
1237
- // src/utils/atomic-write.ts
1238
- var primitives = createPersistencePrimitives({
1239
- createLockTimeoutError: ({ targetPath, timeoutMs }) => new FsError({
1240
- message: `Timed out waiting for file lock: ${targetPath}`,
1241
- code: "FS_ATOMIC_WRITE_FAILED",
1242
- path: targetPath,
1243
- context: { timeoutMs }
1244
- })
1245
- });
1246
- var atomicWrite = primitives.atomicWrite;
1247
- var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
1248
- var ensureDir = primitives.ensureDir;
1249
- var withFileLock = primitives.withFileLock;
1250
-
1251
- // src/utils/pid.ts
1252
- function isPidAlive(pid) {
1253
- if (!Number.isInteger(pid) || pid <= 0) return false;
1254
- if (pid === process.pid) return true;
1255
- try {
1256
- process.kill(pid, 0);
1257
- return true;
1258
- } catch (err) {
1259
- const code = err.code;
1260
- if (code === "EPERM") return true;
1261
- return false;
1262
- }
1263
- }
1264
-
1265
- // src/session-catalog/store-rebuild.ts
1266
- import * as fs4 from "node:fs";
1267
- import * as path4 from "node:path";
1268
-
1269
- // src/utils/session-scoped-path.ts
1270
- var SESSION_SIDECAR_JSONL_SUFFIXES = [
1271
- ".replay.jsonl",
1272
- ".audit.jsonl",
1273
- ".annotations.jsonl"
1274
- ];
1275
- var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
1276
- "_index.jsonl",
1277
- "_mailbox.jsonl"
1278
- ]);
1279
- function isSessionTranscriptFileName(name) {
1280
- if (!name.endsWith(".jsonl")) return false;
1281
- if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
1282
- return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
1283
- }
1284
-
1285
- // src/session-catalog/store-schema.ts
1286
- import { createHash as createHash2, timingSafeEqual } from "node:crypto";
1287
- var SCHEMA_VERSION = 1;
1288
- var MAX_LEASE_MS = 12e4;
1289
- var MAX_RESERVATION_MS = 6e4;
1290
- var MAX_MAINTENANCE_MS = 5 * 6e4;
1291
- var MAX_PAGE = 1e3;
1292
- function hashSecret(secret) {
1293
- return createHash2("sha256").update(secret).digest("hex");
1294
- }
1295
- function secretMatches(secret, expectedHex) {
1296
- const actual = Buffer.from(hashSecret(secret), "hex");
1297
- const expected = Buffer.from(expectedHex, "hex");
1298
- return actual.length === expected.length && timingSafeEqual(actual, expected);
1299
- }
1300
- function boundedMs(value, fallback, max) {
1301
- return Math.min(max, Math.max(1e3, Number.isFinite(value) ? Math.floor(value) : fallback));
1302
- }
1303
- function parseJson(value) {
1304
- return JSON.parse(value);
1305
- }
1306
- function conflict(message) {
1307
- const error = new Error(message);
1308
- error.name = "SessionOwnershipConflictError";
1309
- return error;
1310
- }
1311
- function assertId(value, label = "session id") {
1312
- if (!value || value.length > 256 || value.includes("\\") || value.startsWith("/") || value.includes("..")) {
1313
- throw new TypeError(`Invalid ${label}`);
1314
- }
1315
- }
1316
- function boundPresenceValue(value, depth) {
1317
- if (typeof value === "string") {
1318
- return value.length <= 6e3 ? value : `${value.slice(0, 5988)}\u2026[truncated]`;
1319
- }
1320
- if (value === null || typeof value !== "object") return value;
1321
- if (depth >= 8) return "[truncated depth]";
1322
- if (Array.isArray(value)) {
1323
- return value.slice(0, 64).map((item) => boundPresenceValue(item, depth + 1));
1324
- }
1325
- const result = {};
1326
- for (const [key, item] of Object.entries(value).slice(0, 64)) {
1327
- result[key] = boundPresenceValue(item, depth + 1);
1328
- }
1329
- return result;
1330
- }
1331
- function configureCatalogDatabase(db) {
1332
- db.exec(
1333
- "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;"
1334
- );
1335
- }
1336
- function initializeCatalogSchema(db) {
1337
- db.exec(`
1338
- CREATE TABLE IF NOT EXISTS catalog_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
1339
- CREATE TABLE IF NOT EXISTS sessions (
1340
- session_id TEXT PRIMARY KEY,
1341
- transcript_relative_path TEXT NOT NULL,
1342
- summary_relative_path TEXT NOT NULL,
1343
- summary_json TEXT NOT NULL,
1344
- transcript_size INTEGER NOT NULL DEFAULT 0,
1345
- transcript_mtime_ms REAL NOT NULL DEFAULT 0,
1346
- summary_revision INTEGER NOT NULL DEFAULT 1,
1347
- indexed_at TEXT NOT NULL,
1348
- damaged INTEGER NOT NULL DEFAULT 0
1349
- );
1350
- CREATE TABLE IF NOT EXISTS session_leases (
1351
- session_id TEXT PRIMARY KEY,
1352
- lease_id TEXT NOT NULL UNIQUE,
1353
- lease_secret_hash TEXT NOT NULL,
1354
- owner_instance_id TEXT NOT NULL,
1355
- owner_pid INTEGER NOT NULL,
1356
- owner_started_at TEXT NOT NULL,
1357
- entry_json TEXT NOT NULL,
1358
- agent_revision INTEGER NOT NULL DEFAULT 0,
1359
- status TEXT NOT NULL,
1360
- last_heartbeat_at INTEGER NOT NULL,
1361
- lease_expires_at INTEGER NOT NULL
1362
- );
1363
- CREATE TABLE IF NOT EXISTS resume_reservations (
1364
- reservation_id TEXT PRIMARY KEY,
1365
- target_session_id TEXT NOT NULL UNIQUE,
1366
- requester_instance_id TEXT NOT NULL,
1367
- current_session_id TEXT,
1368
- created_at INTEGER NOT NULL,
1369
- expires_at INTEGER NOT NULL
1370
- );
1371
- CREATE TABLE IF NOT EXISTS maintenance_leases (
1372
- session_id TEXT PRIMARY KEY,
1373
- operation TEXT NOT NULL,
1374
- holder_id TEXT NOT NULL,
1375
- lease_id TEXT NOT NULL UNIQUE,
1376
- acquired_at INTEGER NOT NULL,
1377
- expires_at INTEGER NOT NULL
1378
- );
1379
- CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(indexed_at DESC);
1380
- CREATE INDEX IF NOT EXISTS idx_leases_expiry ON session_leases(lease_expires_at);
1381
- CREATE INDEX IF NOT EXISTS idx_reservations_expiry ON resume_reservations(expires_at);
1382
- CREATE INDEX IF NOT EXISTS idx_maintenance_expiry ON maintenance_leases(expires_at);
1383
- `);
1384
- db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("schema_version", String(SCHEMA_VERSION));
1385
- db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("generation", "0");
1386
- const row = db.prepare("SELECT value FROM catalog_meta WHERE key=?").get("schema_version");
1387
- if (Number(row.value) !== SCHEMA_VERSION)
1388
- throw new Error(`Unsupported session catalog schema ${row.value}`);
1389
- }
1390
-
1391
- // src/session-catalog/store-rebuild.ts
1392
- function walkSessionFiles(root, suffix) {
1393
- const result = [];
1394
- const visit = (dir) => {
1395
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1396
- if (entry.isDirectory()) {
1397
- if (entry.name !== "_cas" && entry.name !== "_trash") visit(path4.join(dir, entry.name));
1398
- } else if (entry.isFile() && entry.name.endsWith(suffix))
1399
- result.push(path4.join(dir, entry.name));
1400
- }
1401
- };
1402
- visit(root);
1403
- return result;
1404
- }
1405
- function summarizeTranscriptFile(transcriptFilePath, id) {
1406
- const lines = fs4.readFileSync(transcriptFilePath, "utf8").split(/\r?\n/);
1407
- let start;
1408
- let endedAt;
1409
- let lastActivityAt;
1410
- let messageCount = 0;
1411
- let iterationCount = 0;
1412
- let toolCallCount = 0;
1413
- let compactionCount = 0;
1414
- let tokenTotal = 0;
1415
- for (const line of lines) {
1416
- if (!line) continue;
1417
- let event;
1418
- try {
1419
- event = parseJson(line);
1420
- } catch {
1421
- continue;
1422
- }
1423
- lastActivityAt = event.ts;
1424
- if (event.type === "session_start") start = event;
1425
- if (event.type === "session_end") endedAt = event.ts;
1426
- if (event.type === "message_appended" && (event.message.role === "user" || event.message.role === "assistant"))
1427
- messageCount++;
1428
- if (event.type === "llm_response") {
1429
- iterationCount++;
1430
- tokenTotal += event.usage.input + event.usage.output + (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
1431
- }
1432
- if (event.type === "tool_call_end") toolCallCount++;
1433
- if (event.type === "compaction") compactionCount++;
1434
- }
1435
- if (!start) throw new Error("missing session_start");
1436
- return {
1437
- id,
1438
- title: id,
1439
- startedAt: start.ts,
1440
- ...endedAt ? { endedAt } : {},
1441
- model: start.model,
1442
- provider: start.provider,
1443
- tokenTotal,
1444
- ...lastActivityAt ? { lastActivityAt } : {},
1445
- messageCount,
1446
- iterationCount,
1447
- toolCallCount,
1448
- compactionCount
1449
- };
1450
- }
1451
- function rebuildCatalogIndex(db, sessionsDir, containedPathFn, transactionFn, bumpGenerationFn) {
1452
- const summaries = walkSessionFiles(sessionsDir, ".summary.json");
1453
- const transcripts = walkSessionFiles(sessionsDir, ".jsonl").filter(
1454
- (file) => isSessionTranscriptFileName(path4.basename(file))
1455
- );
1456
- const ids = /* @__PURE__ */ new Set();
1457
- for (const file of [...summaries, ...transcripts]) {
1458
- const relative2 = path4.relative(sessionsDir, file).replaceAll("\\", "/");
1459
- ids.add(relative2.replace(/\.summary\.json$|\.jsonl$/, ""));
1460
- }
1461
- let indexed = 0;
1462
- let damaged = 0;
1463
- transactionFn(() => {
1464
- db.prepare("DELETE FROM sessions").run();
1465
- for (const id of ids) {
1466
- try {
1467
- const summaryFile = containedPathFn(`${id}.summary.json`);
1468
- const summary = fs4.existsSync(summaryFile) ? parseJson(fs4.readFileSync(summaryFile, "utf8")) : summarizeTranscriptFile(containedPathFn(`${id}.jsonl`), id);
1469
- if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
1470
- const transcript = containedPathFn(`${id}.jsonl`);
1471
- const stat = fs4.existsSync(transcript) ? fs4.statSync(transcript) : void 0;
1472
- const now = (/* @__PURE__ */ new Date()).toISOString();
1473
- db.prepare(
1474
- "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)"
1475
- ).run(
1476
- id,
1477
- `${id}.jsonl`,
1478
- `${id}.summary.json`,
1479
- JSON.stringify(summary),
1480
- stat?.size ?? 0,
1481
- stat?.mtimeMs ?? 0,
1482
- 1,
1483
- now
1484
- );
1485
- indexed++;
1486
- } catch {
1487
- const now = (/* @__PURE__ */ new Date()).toISOString();
1488
- const fallback = {
1489
- id,
1490
- title: id,
1491
- startedAt: now,
1492
- model: "",
1493
- provider: "",
1494
- tokenTotal: 0
1495
- };
1496
- db.prepare(
1497
- "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,1,?,1)"
1498
- ).run(id, `${id}.jsonl`, `${id}.summary.json`, JSON.stringify(fallback), now);
1499
- damaged++;
1500
- }
1501
- }
1502
- db.prepare(
1503
- "INSERT INTO catalog_meta(key,value) VALUES ('last_reconciliation',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
1504
- ).run((/* @__PURE__ */ new Date()).toISOString());
1505
- bumpGenerationFn();
1506
- });
1507
- return { indexed, damaged };
1508
- }
1509
-
1510
- // src/session-catalog/store.ts
1511
- var SessionCatalogStore = class {
1512
- constructor(projectDir) {
1513
- this.projectDir = projectDir;
1514
- this.sessionsDir = path5.join(projectDir, "sessions");
1515
- fs5.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
1516
- this.databasePath = path5.join(this.sessionsDir, "catalog.sqlite");
1517
- const Database = loadDatabaseSync();
1518
- this.db = new Database(this.databasePath);
1519
- try {
1520
- this.configureDatabase();
1521
- this.initialize();
1522
- } catch (error) {
1523
- const message = error instanceof Error ? error.message : String(error);
1524
- if (!/SQLITE_CORRUPT|SQLITE_NOTADB|database disk image is malformed|file is not a database/i.test(
1525
- message
1526
- )) {
1527
- this.db.close();
1528
- throw error;
1529
- }
1530
- this.db.close();
1531
- const quarantine = `${this.databasePath}.corrupt-${Date.now()}`;
1532
- try {
1533
- fs5.renameSync(this.databasePath, quarantine);
1534
- } catch {
1535
- }
1536
- for (const suffix of ["-wal", "-shm"]) {
1537
- try {
1538
- fs5.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
1539
- } catch {
1540
- }
1541
- }
1542
- this.db = new Database(this.databasePath);
1543
- this.configureDatabase();
1544
- this.initialize();
1545
- }
1546
- this.reapExpired();
1547
- const rowCount = Number(
1548
- this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get().count
1549
- );
1550
- if (rowCount === 0 && walkSessionFiles(this.sessionsDir, ".jsonl").some((file) => !file.endsWith("_index.jsonl"))) {
1551
- this.rebuildCatalog();
1552
- }
1553
- }
1554
- projectDir;
1555
- databasePath;
1556
- sessionsDir;
1557
- db;
1558
- scrubber = new DefaultSecretScrubber();
1559
- configureDatabase() {
1560
- configureCatalogDatabase(this.db);
1561
- }
1562
- close() {
1563
- this.db.close();
1564
- }
1565
- initialize() {
1566
- initializeCatalogSchema(this.db);
1567
- }
1568
- transaction(run) {
1569
- this.db.exec("BEGIN IMMEDIATE");
1570
- try {
1571
- const result = run();
1572
- this.db.exec("COMMIT");
1573
- return result;
1574
- } catch (error) {
1575
- this.db.exec("ROLLBACK");
1576
- throw error;
1577
- }
1578
- }
1579
- bumpGeneration() {
1580
- this.db.prepare("UPDATE catalog_meta SET value=CAST(value AS INTEGER)+1 WHERE key='generation'").run();
1581
- return this.generation();
1582
- }
1583
- generation() {
1584
- const row = this.db.prepare("SELECT value FROM catalog_meta WHERE key='generation'").get();
1585
- return Number(row.value) || 0;
1586
- }
1587
- reapExpired(now = Date.now()) {
1588
- this.db.prepare("DELETE FROM resume_reservations WHERE expires_at<=?").run(now);
1589
- this.db.prepare("DELETE FROM maintenance_leases WHERE expires_at<=?").run(now);
1590
- const rows = this.db.prepare("SELECT * FROM session_leases WHERE lease_expires_at<=?").all(now);
1591
- for (const row of rows) {
1592
- if (!isPidAlive(row.owner_pid)) {
1593
- this.db.prepare("DELETE FROM session_leases WHERE session_id=? AND lease_id=?").run(row.session_id, row.lease_id);
1594
- } else if (row.status !== "lost") {
1595
- this.db.prepare("UPDATE session_leases SET status='lost' WHERE session_id=? AND lease_id=?").run(row.session_id, row.lease_id);
1596
- }
1597
- }
1598
- }
1599
- maintenanceExists(sessionId) {
1600
- return Boolean(
1601
- this.db.prepare("SELECT 1 AS yes FROM maintenance_leases WHERE session_id=? AND expires_at>?").get(sessionId, Date.now())
1602
- );
1603
- }
1604
- leaseRow(sessionId) {
1605
- return this.db.prepare("SELECT * FROM session_leases WHERE session_id=?").get(sessionId);
1606
- }
1607
- foreignLiveLease(sessionId, callerPid = process.pid) {
1608
- const live = this.leaseRow(sessionId);
1609
- return Boolean(live) && live.owner_pid !== callerPid;
1610
- }
1611
- verifyCredential(credential) {
1612
- assertId(credential.sessionId);
1613
- const row = this.leaseRow(credential.sessionId);
1614
- if (!row || row.lease_id !== credential.leaseId || row.owner_instance_id !== credential.ownerInstanceId || !secretMatches(credential.leaseSecret, row.lease_secret_hash)) {
1615
- throw conflict(`Session ${credential.sessionId} lease proof is invalid or no longer owned`);
1616
- }
1617
- return row;
1618
- }
1619
- createLease(entry, ownerInstanceId, leaseMs) {
1620
- assertId(entry.sessionId);
1621
- if (!ownerInstanceId || ownerInstanceId.length > 256)
1622
- throw new TypeError("Invalid owner instance id");
1623
- if (!Number.isSafeInteger(entry.pid) || entry.pid <= 0)
1624
- throw new TypeError("Invalid owner pid");
1625
- const now = Date.now();
1626
- const leaseId = randomUUID2();
1627
- const leaseSecret = randomBytes(32).toString("hex");
1628
- const expiresAt = now + boundedMs(leaseMs, SESSION_CATALOG_DEFAULT_LEASE_MS, MAX_LEASE_MS);
1629
- this.db.prepare(`INSERT INTO session_leases(
1630
- session_id,lease_id,lease_secret_hash,owner_instance_id,owner_pid,owner_started_at,
1631
- entry_json,agent_revision,status,last_heartbeat_at,lease_expires_at
1632
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(
1633
- entry.sessionId,
1634
- leaseId,
1635
- hashSecret(leaseSecret),
1636
- ownerInstanceId,
1637
- entry.pid,
1638
- entry.startedAt,
1639
- JSON.stringify(entry),
1640
- 0,
1641
- entry.status,
1642
- now,
1643
- expiresAt
1644
- );
1645
- return { sessionId: entry.sessionId, leaseId, leaseSecret, ownerInstanceId, expiresAt };
1646
- }
1647
- claimNew(entry, ownerInstanceId, leaseMs) {
1648
- return this.transaction(() => {
1649
- this.reapExpired();
1650
- const existing = this.leaseRow(entry.sessionId);
1651
- if (existing)
1652
- throw conflict(
1653
- `Session ${entry.sessionId} is already open in another running wstack (pid ${existing.owner_pid}).`
1654
- );
1655
- if (this.maintenanceExists(entry.sessionId))
1656
- throw conflict(`Session ${entry.sessionId} is under maintenance`);
1657
- const reserved = this.db.prepare(
1658
- "SELECT 1 AS yes FROM resume_reservations WHERE target_session_id=? AND expires_at>?"
1659
- ).get(entry.sessionId, Date.now());
1660
- if (reserved) throw conflict(`Session ${entry.sessionId} is reserved for resume`);
1661
- const credential = this.createLease(entry, ownerInstanceId, leaseMs);
1662
- this.bumpGeneration();
1663
- return credential;
1664
- });
1665
- }
1666
- reconnectLease(credential) {
1667
- return this.transaction(() => {
1668
- const row = this.verifyCredential(credential);
1669
- if (row.owner_pid !== process.pid && !isPidAlive(row.owner_pid))
1670
- throw conflict(`Session ${credential.sessionId} owner process is no longer alive`);
1671
- const expiresAt = Date.now() + SESSION_CATALOG_DEFAULT_LEASE_MS;
1672
- this.db.prepare(
1673
- "UPDATE session_leases SET lease_expires_at=?,last_heartbeat_at=?,status=? WHERE session_id=? AND lease_id=?"
1674
- ).run(
1675
- expiresAt,
1676
- Date.now(),
1677
- row.status === "lost" ? "idle" : row.status,
1678
- row.session_id,
1679
- row.lease_id
1680
- );
1681
- return { ...credential, expiresAt };
1682
- });
1683
- }
1684
- reserveResume(targetSessionId, requesterInstanceId, currentSessionId, reservationMs) {
1685
- assertId(targetSessionId);
1686
- return this.transaction(() => {
1687
- this.reapExpired();
1688
- const live = this.leaseRow(targetSessionId);
1689
- if (live)
1690
- throw conflict(
1691
- `Session ${targetSessionId} is already open in another running wstack (pid ${live.owner_pid}).`
1692
- );
1693
- if (this.maintenanceExists(targetSessionId))
1694
- throw conflict(`Session ${targetSessionId} is under maintenance`);
1695
- const catalog = this.db.prepare("SELECT 1 AS yes FROM sessions WHERE session_id=?").get(targetSessionId);
1696
- if (!catalog && !fs5.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
1697
- throw new Error(`Session not found: ${targetSessionId}`);
1698
- const reservationId = randomUUID2();
1699
- const now = Date.now();
1700
- const expiresAt = now + boundedMs(reservationMs, SESSION_CATALOG_DEFAULT_RESERVATION_MS, MAX_RESERVATION_MS);
1701
- try {
1702
- this.db.prepare(
1703
- "INSERT INTO resume_reservations(reservation_id,target_session_id,requester_instance_id,current_session_id,created_at,expires_at) VALUES (?,?,?,?,?,?)"
1704
- ).run(
1705
- reservationId,
1706
- targetSessionId,
1707
- requesterInstanceId,
1708
- currentSessionId ?? null,
1709
- now,
1710
- expiresAt
1711
- );
1712
- } catch {
1713
- throw conflict(`Session ${targetSessionId} is already reserved for resume`);
1714
- }
1715
- this.bumpGeneration();
1716
- return { reservationId, targetSessionId, requesterInstanceId, expiresAt };
1717
- });
1718
- }
1719
- activateReservation(reservation, entry, leaseMs) {
1720
- return this.transaction(() => {
1721
- this.reapExpired();
1722
- const row = this.db.prepare("SELECT * FROM resume_reservations WHERE reservation_id=?").get(reservation.reservationId);
1723
- if (!row || row.target_session_id !== reservation.targetSessionId || row.requester_instance_id !== reservation.requesterInstanceId || row.expires_at <= Date.now())
1724
- throw conflict("Resume reservation expired or is not owned by this requester");
1725
- if (entry.sessionId !== row.target_session_id)
1726
- throw new TypeError("Reservation target and session entry differ");
1727
- if (this.leaseRow(entry.sessionId) || this.maintenanceExists(entry.sessionId))
1728
- throw conflict(`Session ${entry.sessionId} can no longer be activated`);
1729
- const credential = this.createLease(entry, reservation.requesterInstanceId, leaseMs);
1730
- this.db.prepare("DELETE FROM resume_reservations WHERE reservation_id=?").run(row.reservation_id);
1731
- this.bumpGeneration();
1732
- return credential;
1733
- });
1734
- }
1735
- cancelReservation(reservationId, requesterInstanceId) {
1736
- this.db.prepare("DELETE FROM resume_reservations WHERE reservation_id=? AND requester_instance_id=?").run(reservationId, requesterInstanceId);
1737
- }
1738
- heartbeat(credential, status) {
1739
- return this.transaction(() => {
1740
- const row = this.verifyCredential(credential);
1741
- const expiresAt = Date.now() + SESSION_CATALOG_DEFAULT_LEASE_MS;
1742
- const nextStatus = status ?? (row.status === "closing" ? "closing" : row.status === "lost" ? "idle" : row.status);
1743
- const entry = parseJson(row.entry_json);
1744
- entry.status = nextStatus;
1745
- entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
1746
- this.db.prepare(
1747
- "UPDATE session_leases SET status=?,entry_json=?,last_heartbeat_at=?,lease_expires_at=? WHERE session_id=? AND lease_id=?"
1748
- ).run(
1749
- nextStatus,
1750
- JSON.stringify(entry),
1751
- Date.now(),
1752
- expiresAt,
1753
- row.session_id,
1754
- row.lease_id
1755
- );
1756
- return { ...credential, expiresAt };
1757
- });
1758
- }
1759
- publishAgents(credential, revision, agents) {
1760
- if (!Number.isSafeInteger(revision) || revision < 0)
1761
- throw new TypeError("Invalid presence revision");
1762
- if (!Array.isArray(agents) || agents.length > SESSION_CATALOG_MAX_AGENTS)
1763
- throw new TypeError(`Agent snapshot exceeds ${SESSION_CATALOG_MAX_AGENTS} agents`);
1764
- const boundedAgents = boundPresenceValue(
1765
- this.scrubber.scrubObject(agents),
1766
- 0
1767
- );
1768
- const encoded = JSON.stringify(boundedAgents);
1769
- if (encoded.length > 1024 * 1024) throw new TypeError("Agent snapshot exceeds 1 MiB");
1770
- return this.transaction(() => {
1771
- const row = this.verifyCredential(credential);
1772
- if (revision <= row.agent_revision) return { accepted: false, revision: row.agent_revision };
1773
- const entry = parseJson(row.entry_json);
1774
- entry.agents = boundedAgents;
1775
- entry.agentCount = boundedAgents.length;
1776
- entry.status = boundedAgents.some((agent) => agent.status !== "idle") ? "active" : "idle";
1777
- entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
1778
- this.db.prepare(
1779
- "UPDATE session_leases SET entry_json=?,agent_revision=?,status=?,last_heartbeat_at=? WHERE session_id=? AND lease_id=?"
1780
- ).run(
1781
- JSON.stringify(entry),
1782
- revision,
1783
- entry.status,
1784
- Date.now(),
1785
- row.session_id,
1786
- row.lease_id
1787
- );
1788
- this.bumpGeneration();
1789
- return { accepted: true, revision };
1790
- });
1791
- }
1792
- markClosing(credential) {
1793
- const row = this.verifyCredential(credential);
1794
- const entry = parseJson(row.entry_json);
1795
- entry.status = "closing";
1796
- entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
1797
- this.db.prepare(
1798
- "UPDATE session_leases SET status='closing',entry_json=?,last_heartbeat_at=? WHERE session_id=? AND lease_id=?"
1799
- ).run(JSON.stringify(entry), Date.now(), row.session_id, row.lease_id);
1800
- this.bumpGeneration();
1801
- }
1802
- release(credential) {
1803
- this.transaction(() => {
1804
- const row = this.verifyCredential(credential);
1805
- this.db.prepare("DELETE FROM session_leases WHERE session_id=? AND lease_id=?").run(row.session_id, row.lease_id);
1806
- this.bumpGeneration();
1807
- });
1808
- }
1809
- listLive() {
1810
- this.reapExpired();
1811
- return this.db.prepare(
1812
- "SELECT entry_json,status,last_heartbeat_at FROM session_leases ORDER BY last_heartbeat_at DESC"
1813
- ).all().map((row) => ({
1814
- ...parseJson(row.entry_json),
1815
- status: row.status,
1816
- lastHeartbeatAt: new Date(row.last_heartbeat_at).toISOString()
1817
- }));
1818
- }
1819
- getLive(sessionId) {
1820
- return this.listLive().find((entry) => entry.sessionId === sessionId) ?? null;
1821
- }
1822
- containedPath(relative2) {
1823
- assertId(relative2.replace(/\.(jsonl|summary\.json)$/, ""), "session path");
1824
- const root = path5.resolve(this.sessionsDir);
1825
- const candidate = path5.resolve(root, relative2);
1826
- const prefix = `${root}${path5.sep}`;
1827
- if (candidate !== root && !(process.platform === "win32" ? candidate.toLowerCase().startsWith(prefix.toLowerCase()) : candidate.startsWith(prefix)))
1828
- throw new TypeError("Session path escapes sessions directory");
1829
- return candidate;
1830
- }
1831
- upsertSummary(summary, transcriptRelativePath = `${summary.id}.jsonl`, summaryRelativePath = `${summary.id}.summary.json`) {
1832
- assertId(summary.id);
1833
- summary = this.scrubber.scrubObject(summary);
1834
- const normalizedTranscript = transcriptRelativePath.replaceAll("\\", "/");
1835
- const normalizedSummary = summaryRelativePath.replaceAll("\\", "/");
1836
- if (normalizedTranscript !== `${summary.id}.jsonl` || normalizedSummary !== `${summary.id}.summary.json`) {
1837
- throw new TypeError("Session catalog paths must match the canonical session identity");
1838
- }
1839
- transcriptRelativePath = normalizedTranscript;
1840
- summaryRelativePath = normalizedSummary;
1841
- const transcript = this.containedPath(transcriptRelativePath);
1842
- const stat = fs5.existsSync(transcript) ? fs5.statSync(transcript) : void 0;
1843
- const now = (/* @__PURE__ */ new Date()).toISOString();
1844
- return this.transaction(() => {
1845
- const prior = this.db.prepare("SELECT summary_revision FROM sessions WHERE session_id=?").get(summary.id);
1846
- const revision = (prior?.summary_revision ?? 0) + 1;
1847
- this.db.prepare(`INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,transcript_size,transcript_mtime_ms,summary_revision,indexed_at,damaged)
1848
- VALUES (?,?,?,?,?,?,?,?,0) ON CONFLICT(session_id) DO UPDATE SET transcript_relative_path=excluded.transcript_relative_path,summary_relative_path=excluded.summary_relative_path,summary_json=excluded.summary_json,transcript_size=excluded.transcript_size,transcript_mtime_ms=excluded.transcript_mtime_ms,summary_revision=excluded.summary_revision,indexed_at=excluded.indexed_at,damaged=0`).run(
1849
- summary.id,
1850
- transcriptRelativePath,
1851
- summaryRelativePath,
1852
- JSON.stringify(summary),
1853
- stat?.size ?? 0,
1854
- stat?.mtimeMs ?? 0,
1855
- revision,
1856
- now
1857
- );
1858
- this.bumpGeneration();
1859
- return {
1860
- ...summary,
1861
- transcriptRelativePath,
1862
- summaryRelativePath,
1863
- transcriptSize: stat?.size ?? 0,
1864
- transcriptMtimeMs: stat?.mtimeMs ?? 0,
1865
- summaryRevision: revision,
1866
- indexedAt: now,
1867
- damaged: false
1868
- };
1869
- });
1870
- }
1871
- catalogRecord(row) {
1872
- return {
1873
- ...parseJson(row.summary_json),
1874
- transcriptRelativePath: row.transcript_relative_path,
1875
- summaryRelativePath: row.summary_relative_path,
1876
- transcriptSize: row.transcript_size,
1877
- transcriptMtimeMs: row.transcript_mtime_ms,
1878
- summaryRevision: row.summary_revision,
1879
- indexedAt: row.indexed_at,
1880
- damaged: row.damaged !== 0
1881
- };
1882
- }
1883
- listCatalog(criteria = {}) {
1884
- const requestedLimit = criteria.limit ?? 100;
1885
- if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
1886
- const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
1887
- const clauses = [];
1888
- const values = [];
1889
- const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
1890
- const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
1891
- const search = criteria.search?.trim();
1892
- if (search) {
1893
- const pattern = literalLike(search);
1894
- clauses.push(
1895
- `(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
1896
- );
1897
- values.push(pattern, pattern, pattern);
1898
- }
1899
- if (criteria.since) {
1900
- clauses.push(`${jsonText("startedAt")}>=?`);
1901
- values.push(criteria.since);
1902
- }
1903
- if (criteria.until) {
1904
- clauses.push(`${jsonText("startedAt")}<=?`);
1905
- values.push(criteria.until);
1906
- }
1907
- if (criteria.provider) {
1908
- clauses.push(`${jsonText("provider")}=?`);
1909
- values.push(criteria.provider);
1910
- }
1911
- if (criteria.model) {
1912
- clauses.push(`${jsonText("model")}=?`);
1913
- values.push(criteria.model);
1914
- }
1915
- if (criteria.minTokens !== void 0) {
1916
- if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
1917
- clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
1918
- values.push(criteria.minTokens);
1919
- }
1920
- if (criteria.titleContains) {
1921
- clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
1922
- values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
1923
- }
1924
- const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
1925
- const rows = this.db.prepare(
1926
- `SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
1927
- ).all(...values, bounded);
1928
- return rows.map((row) => this.catalogRecord(row));
1929
- }
1930
- getSummary(sessionId) {
1931
- const row = this.db.prepare("SELECT * FROM sessions WHERE session_id=?").get(sessionId);
1932
- return row ? this.catalogRecord(row) : null;
1933
- }
1934
- resolveId(query) {
1935
- const normalized = query.trim();
1936
- if (!normalized) throw new Error("Session not found: (empty query)");
1937
- if (this.getSummary(normalized)) return normalized;
1938
- const rows = this.db.prepare(
1939
- "SELECT session_id FROM sessions WHERE session_id LIKE ? OR session_id LIKE ? LIMIT 3"
1940
- ).all(`%/${normalized}`, `${normalized}%`);
1941
- const ids = [...new Set(rows.map((row) => row.session_id))];
1942
- if (ids.length === 1) return ids[0];
1943
- if (ids.length === 0) throw new Error(`Session not found: ${query}`);
1944
- throw new Error(`Ambiguous session id "${query}": ${ids.join(", ")}`);
1945
- }
1946
- async rename(sessionId, name) {
1947
- const current = this.getSummary(this.resolveId(sessionId));
1948
- if (!current) throw new Error(`Session not found: ${sessionId}`);
1949
- const trimmed = name.trim();
1950
- const summary = { ...current };
1951
- for (const key of [
1952
- "transcriptRelativePath",
1953
- "summaryRelativePath",
1954
- "transcriptSize",
1955
- "transcriptMtimeMs",
1956
- "summaryRevision",
1957
- "indexedAt",
1958
- "damaged"
1959
- ])
1960
- delete summary[key];
1961
- const previous = { ...summary };
1962
- if (trimmed) summary.name = this.scrubber.scrub(trimmed).slice(0, 500);
1963
- else delete summary.name;
1964
- const summaryPath = this.containedPath(current.summaryRelativePath);
1965
- fs5.mkdirSync(path5.dirname(summaryPath), { recursive: true, mode: 448 });
1966
- await atomicWrite(summaryPath, `${JSON.stringify(summary)}
1967
- `, { mode: 384 });
1968
- try {
1969
- return this.upsertSummary(
1970
- summary,
1971
- current.transcriptRelativePath,
1972
- current.summaryRelativePath
1973
- );
1974
- } catch (error) {
1975
- await atomicWrite(summaryPath, `${JSON.stringify(previous)}
1976
- `, { mode: 384 }).catch(
1977
- () => void 0
1978
- );
1979
- throw error;
1980
- }
1981
- }
1982
- acquireMaintenance(sessionId, operation, holderId, leaseMs, holderPid) {
1983
- assertId(sessionId);
1984
- return this.transaction(() => {
1985
- this.reapExpired();
1986
- const live = this.leaseRow(sessionId);
1987
- if (live && (operation === "delete" || this.foreignLiveLease(sessionId, holderPid))) {
1988
- throw conflict(`Session ${sessionId} is live`);
1989
- }
1990
- const reservation = this.db.prepare(
1991
- "SELECT 1 AS yes FROM resume_reservations WHERE target_session_id=? AND expires_at>?"
1992
- ).get(sessionId, Date.now());
1993
- if (reservation) throw conflict(`Session ${sessionId} is reserved for resume`);
1994
- const leaseId = randomUUID2();
1995
- const now = Date.now();
1996
- const expiresAt = now + boundedMs(leaseMs, 6e4, MAX_MAINTENANCE_MS);
1997
- try {
1998
- this.db.prepare(
1999
- "INSERT INTO maintenance_leases(session_id,operation,holder_id,lease_id,acquired_at,expires_at) VALUES (?,?,?,?,?,?)"
2000
- ).run(sessionId, operation, holderId, leaseId, now, expiresAt);
2001
- } catch {
2002
- throw conflict(`Session ${sessionId} already has maintenance in progress`);
2003
- }
2004
- return { sessionId, operation, holderId, leaseId, expiresAt };
2005
- });
2006
- }
2007
- releaseMaintenance(lease) {
2008
- this.db.prepare("DELETE FROM maintenance_leases WHERE session_id=? AND lease_id=? AND holder_id=?").run(lease.sessionId, lease.leaseId, lease.holderId);
2009
- }
2010
- delete(sessionId, lease) {
2011
- const row = this.db.prepare(
2012
- "SELECT * FROM maintenance_leases WHERE session_id=? AND lease_id=? AND holder_id=? AND operation=? AND expires_at>?"
2013
- ).get(sessionId, lease.leaseId, lease.holderId, lease.operation, Date.now());
2014
- if (!row || lease.operation !== "delete")
2015
- throw conflict("A valid delete maintenance lease is required");
2016
- const record = this.getSummary(sessionId);
2017
- if (!record) throw new Error(`Session not found: ${sessionId}`);
2018
- const transcript = this.containedPath(record.transcriptRelativePath);
2019
- const artifacts = [
2020
- transcript,
2021
- this.containedPath(record.summaryRelativePath),
2022
- this.containedPath(`${sessionId}.plan.json`),
2023
- this.containedPath(`${sessionId}.tasks.json`),
2024
- this.containedPath(`${sessionId}.todos.json`),
2025
- path5.join(path5.dirname(transcript), path5.basename(sessionId))
2026
- ];
2027
- const trashRoot = path5.join(this.sessionsDir, "_trash", lease.leaseId);
2028
- fs5.mkdirSync(trashRoot, { recursive: true, mode: 448 });
2029
- const moved = [];
2030
- try {
2031
- artifacts.forEach((artifact, index) => {
2032
- if (!fs5.existsSync(artifact)) return;
2033
- const target = path5.join(trashRoot, `${index}-${path5.basename(artifact)}`);
2034
- fs5.renameSync(artifact, target);
2035
- moved.push({ from: artifact, to: target });
2036
- });
2037
- this.transaction(() => {
2038
- const current = this.db.prepare(
2039
- "SELECT 1 AS yes FROM maintenance_leases WHERE session_id=? AND lease_id=? AND holder_id=? AND operation=? AND expires_at>?"
2040
- ).get(sessionId, lease.leaseId, lease.holderId, "delete", Date.now());
2041
- if (!current) throw conflict("Delete maintenance lease expired while staging artifacts");
2042
- this.db.prepare("DELETE FROM sessions WHERE session_id=?").run(sessionId);
2043
- this.db.prepare("DELETE FROM maintenance_leases WHERE session_id=?").run(sessionId);
2044
- this.bumpGeneration();
2045
- });
2046
- } catch (error) {
2047
- for (const item of moved.reverse()) {
2048
- try {
2049
- fs5.mkdirSync(path5.dirname(item.from), { recursive: true, mode: 448 });
2050
- fs5.renameSync(item.to, item.from);
2051
- } catch {
2052
- }
2053
- }
2054
- throw error;
2055
- }
2056
- try {
2057
- fs5.rmSync(trashRoot, { recursive: true, force: true });
2058
- const trashParent = path5.dirname(trashRoot);
2059
- if (fs5.readdirSync(trashParent).length === 0) fs5.rmdirSync(trashParent);
2060
- } catch {
2061
- }
2062
- }
2063
- prune(maxAgeDays, holderId) {
2064
- if (!Number.isFinite(maxAgeDays) || maxAgeDays < 0) throw new TypeError("Invalid prune age");
2065
- const cutoff = Date.now() - maxAgeDays * 864e5;
2066
- const candidates = this.db.prepare("SELECT session_id FROM sessions WHERE transcript_mtime_ms<?").all(cutoff);
2067
- let deleted = 0;
2068
- for (const { session_id: id } of candidates) {
2069
- try {
2070
- const lease = this.acquireMaintenance(id, "delete", holderId);
2071
- this.delete(id, lease);
2072
- deleted++;
2073
- } catch (error) {
2074
- if (error.name !== "SessionOwnershipConflictError") throw error;
2075
- }
2076
- }
2077
- return deleted;
2078
- }
2079
- rebuildCatalog() {
2080
- return rebuildCatalogIndex(
2081
- this.db,
2082
- this.sessionsDir,
2083
- (rel) => this.containedPath(rel),
2084
- (run) => this.transaction(run),
2085
- () => this.bumpGeneration()
2086
- );
2087
- }
2088
- health(base) {
2089
- this.reapExpired();
2090
- const count = (table, where = "") => Number(
2091
- this.db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get().count
2092
- );
2093
- const reconciliation = this.db.prepare("SELECT value FROM catalog_meta WHERE key='last_reconciliation'").get();
2094
- return {
2095
- ...base,
2096
- catalogRows: count("sessions"),
2097
- damagedRows: count("sessions", "WHERE damaged<>0"),
2098
- liveLeases: count("session_leases"),
2099
- reservations: count("resume_reservations"),
2100
- maintenanceLeases: count("maintenance_leases"),
2101
- generation: this.generation(),
2102
- ...reconciliation ? { lastReconciliation: reconciliation.value } : {}
2103
- };
2104
- }
2105
- };
8
+ SessionCatalogStore
9
+ } from "../chunk-AI4LLQ2U.js";
10
+ import {
11
+ SessionCatalogProjectClient,
12
+ resolveSessionCatalogDaemonAvailability,
13
+ resolveSessionCatalogProjectServerUrl
14
+ } from "../chunk-52RTVH4M.js";
15
+ import {
16
+ SESSION_CATALOG_DEFAULT_LEASE_MS,
17
+ SESSION_CATALOG_DEFAULT_RESERVATION_MS,
18
+ SESSION_CATALOG_MAX_AGENTS,
19
+ SESSION_CATALOG_MAX_FRAME_CHARS,
20
+ SESSION_CATALOG_METADATA_FILE,
21
+ SESSION_CATALOG_PROTOCOL_VERSION,
22
+ encodeSessionCatalogMessage,
23
+ ensureSessionCatalogSocketDirectory,
24
+ sessionCatalogProjectServerEndpoint,
25
+ sessionCatalogProjectServerKey,
26
+ sessionCatalogProjectServerMetadataPath
27
+ } from "../chunk-E6I5RXVK.js";
28
+ import "../chunk-GM54GJBU.js";
29
+ import "../chunk-W4A6QA3D.js";
30
+ import "../chunk-3E244UDA.js";
31
+ import "../chunk-AD7ZFZCS.js";
32
+ import "../chunk-TRT2HRWZ.js";
33
+ import "../chunk-LX5QWLBL.js";
34
+ import "../chunk-FAYRX2AD.js";
35
+ import "../chunk-7ZHZE5EO.js";
36
+ import "../chunk-YNCVSFGF.js";
37
+ import "../chunk-WOEZV3Q4.js";
38
+ import "../chunk-TBRL2RCC.js";
2106
39
  export {
2107
40
  ProjectSessionRegistry,
2108
41
  SESSION_CATALOG_DEFAULT_LEASE_MS,