pushary 1.9.75 → 1.9.77

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 (126) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{api-L3BIISGS.js → api-IGMCPM35.js} +10 -10
  3. package/dist/bin/pushary-bell-hook.js +26 -25
  4. package/dist/bin/pushary-bell.js +10 -10
  5. package/dist/bin/pushary-claude.js +27 -26
  6. package/dist/bin/pushary-clean.js +33 -33
  7. package/dist/bin/pushary-codex-bridge.js +21 -21
  8. package/dist/bin/pushary-codex-hook.js +31 -30
  9. package/dist/bin/pushary-codex-session.js +8 -8
  10. package/dist/bin/pushary-codex.js +13 -13
  11. package/dist/bin/pushary-connect.js +26 -26
  12. package/dist/bin/pushary-cowork.js +5 -5
  13. package/dist/bin/pushary-daemon.js +123 -66
  14. package/dist/bin/pushary-disconnect.js +25 -25
  15. package/dist/bin/pushary-display-hook.js +17 -16
  16. package/dist/bin/pushary-doctor.js +41 -41
  17. package/dist/bin/pushary-elicitation-hook.js +26 -25
  18. package/dist/bin/pushary-gemini-bridge.js +15 -15
  19. package/dist/bin/pushary-gemini-hook.js +28 -27
  20. package/dist/bin/pushary-hook.js +30 -29
  21. package/dist/bin/pushary-login.js +20 -20
  22. package/dist/bin/pushary-logout.js +24 -24
  23. package/dist/bin/pushary-mcp.js +11 -11
  24. package/dist/bin/pushary-mode.js +15 -15
  25. package/dist/bin/pushary-notification-hook.js +25 -24
  26. package/dist/bin/pushary-opencode-hook.js +27 -26
  27. package/dist/bin/pushary-permission-denied-hook.js +30 -29
  28. package/dist/bin/pushary-permission-hook.js +30 -29
  29. package/dist/bin/pushary-post-hook.js +25 -24
  30. package/dist/bin/pushary-prompt-hook.js +25 -24
  31. package/dist/bin/pushary-session-end-hook.js +27 -26
  32. package/dist/bin/pushary-session-start-hook.js +35 -34
  33. package/dist/bin/pushary-setup.js +50 -267
  34. package/dist/bin/pushary-stats.js +5 -5
  35. package/dist/bin/pushary-status.js +31 -31
  36. package/dist/bin/pushary-stop-hook.js +25 -24
  37. package/dist/bin/pushary-stopfailure-hook.js +25 -24
  38. package/dist/bin/pushary-suggestions.js +15 -15
  39. package/dist/bin/pushary-transcript-register.js +47 -18
  40. package/dist/bin/pushary-transcripts.js +19 -19
  41. package/dist/bin/pushary-upgrade.js +39 -39
  42. package/dist/bin/pushary-wait.js +15 -15
  43. package/dist/bin/pushary.js +7 -7
  44. package/dist/{chunk-JCZWWBBA.js → chunk-2IOQHB3C.js} +5 -5
  45. package/dist/{chunk-UIS332AZ.js → chunk-2OWG667E.js} +1 -1
  46. package/dist/{chunk-UEJTWQVF.js → chunk-2YRNRYZW.js} +1 -1
  47. package/dist/{chunk-UU3CDLIX.js → chunk-3UYMJZXV.js} +2 -2
  48. package/dist/{chunk-HXV2SY2V.js → chunk-4ZXFOT44.js} +2 -2
  49. package/dist/{chunk-L2FQBHIR.js → chunk-6WOG2SCC.js} +3 -2
  50. package/dist/{chunk-6BFLDHPM.js → chunk-6XOEYBWJ.js} +3 -3
  51. package/dist/{chunk-SOHJ37NA.js → chunk-7AQEWD2J.js} +3 -3
  52. package/dist/{chunk-VGHWK5Z2.js → chunk-7CU7OGWP.js} +263 -5
  53. package/dist/{chunk-GAAN3Y7V.js → chunk-7HYNAA6B.js} +1 -1
  54. package/dist/{chunk-EVBG5YUO.js → chunk-A3BF7SGM.js} +3 -3
  55. package/dist/{chunk-AUDYAVNM.js → chunk-ABAWD6QM.js} +3 -3
  56. package/dist/{chunk-Z7ORL6SC.js → chunk-BUBC4R5J.js} +2 -2
  57. package/dist/{chunk-RPKFXNOJ.js → chunk-CIMFZIGP.js} +2 -2
  58. package/dist/{chunk-SIJHPGUW.js → chunk-CKUFXQUJ.js} +3 -3
  59. package/dist/{chunk-P4TWTQ6V.js → chunk-CS5PJXL7.js} +6 -6
  60. package/dist/{chunk-LRYZXY4V.js → chunk-CU65AFT4.js} +1 -1
  61. package/dist/{chunk-M7BXAWUC.js → chunk-D5NLXF72.js} +3 -3
  62. package/dist/{chunk-LIDUPDEY.js → chunk-DYDA77FS.js} +4 -4
  63. package/dist/{chunk-4DH54LS7.js → chunk-EFHCTIFT.js} +1 -1
  64. package/dist/{chunk-EOGVWCLF.js → chunk-ESHMWZXX.js} +5 -5
  65. package/dist/{chunk-T6RWREWU.js → chunk-EV47UGDK.js} +1 -1
  66. package/dist/{chunk-UBANQ4EZ.js → chunk-FBLVK2WR.js} +13 -13
  67. package/dist/{chunk-FEC3JUIH.js → chunk-FXQCSU4L.js} +3 -3
  68. package/dist/{chunk-I73UQU7K.js → chunk-GI3PX2JU.js} +1 -1
  69. package/dist/{chunk-SFJOGZQV.js → chunk-GL34UITI.js} +3 -3
  70. package/dist/{chunk-4T25BBBO.js → chunk-GOJO2OLP.js} +4 -4
  71. package/dist/{chunk-32JJ5KV3.js → chunk-GV7FB5L3.js} +4 -4
  72. package/dist/{chunk-3SAF4QXU.js → chunk-H4GOJIOK.js} +4 -4
  73. package/dist/{chunk-QDUDXJAF.js → chunk-HGVUTO67.js} +3 -3
  74. package/dist/{chunk-53D6FFVE.js → chunk-IHT33GZN.js} +1 -1
  75. package/dist/{chunk-QRS7FQHE.js → chunk-INBG473K.js} +8 -2
  76. package/dist/{chunk-E5XYFIYL.js → chunk-J2NPFRDI.js} +6 -6
  77. package/dist/{chunk-DCHZ5JOI.js → chunk-JJPBWBAF.js} +6 -6
  78. package/dist/{chunk-DX65IDBI.js → chunk-JLYVYHD2.js} +1 -1
  79. package/dist/{chunk-DARPCQPY.js → chunk-K2MW33DG.js} +2 -2
  80. package/dist/{chunk-JFVLMWKY.js → chunk-KUI2KGRZ.js} +9 -9
  81. package/dist/{chunk-BGEUAJYL.js → chunk-LD5LSJ2B.js} +2 -2
  82. package/dist/{chunk-JOMG6J64.js → chunk-LIEPMKTX.js} +1 -1
  83. package/dist/chunk-MHUKYYYR.js +239 -0
  84. package/dist/{chunk-I6TIZMM3.js → chunk-MQBQRXTN.js} +5 -5
  85. package/dist/{chunk-DOG3BQFO.js → chunk-MRTGCXG4.js} +6 -6
  86. package/dist/{chunk-EH5OLIP2.js → chunk-NHIEDRZR.js} +236 -20
  87. package/dist/{chunk-PV5XEBFI.js → chunk-NNV2XS3Y.js} +6 -6
  88. package/dist/{chunk-WL3V2UV4.js → chunk-OJ4EEE2B.js} +6 -6
  89. package/dist/{chunk-D4X4BQVQ.js → chunk-OXA2P5NJ.js} +10 -10
  90. package/dist/{chunk-BRG4XS2H.js → chunk-P4IFREOO.js} +1 -1
  91. package/dist/{chunk-MD4UJICD.js → chunk-P7CGVHH5.js} +5 -5
  92. package/dist/{chunk-ZX3KEXO3.js → chunk-PAPUUZOP.js} +626 -164
  93. package/dist/{chunk-PGLNE556.js → chunk-PGZFXNRJ.js} +1 -1
  94. package/dist/{chunk-3PPL64AG.js → chunk-PKHNZTHY.js} +9 -4
  95. package/dist/{chunk-NCTABQMC.js → chunk-PUF3BL6C.js} +1 -1
  96. package/dist/{chunk-EM2RSWDA.js → chunk-PZWC3JXW.js} +5 -5
  97. package/dist/{chunk-2J4IVUEA.js → chunk-PZXQUV3P.js} +4 -4
  98. package/dist/{chunk-7O54GE5C.js → chunk-QBCV27CM.js} +3 -3
  99. package/dist/{chunk-SNUHX5GQ.js → chunk-QGOY3HNV.js} +6 -6
  100. package/dist/{chunk-7FNSQMQ2.js → chunk-QS2IKPQW.js} +4 -4
  101. package/dist/{chunk-CI6MDBQB.js → chunk-QVP6F6BX.js} +7 -7
  102. package/dist/{chunk-CLHRSK5L.js → chunk-RYDT6Y32.js} +4 -4
  103. package/dist/{chunk-HRRGIVIL.js → chunk-S2UHLCF5.js} +3 -3
  104. package/dist/{chunk-R7V6BPEV.js → chunk-SNHAXVCJ.js} +1 -1
  105. package/dist/{chunk-4GWKKUQ5.js → chunk-SUOW4HJB.js} +5 -5
  106. package/dist/{chunk-TPYZFYQ5.js → chunk-TK3XMPG2.js} +4 -4
  107. package/dist/{chunk-XO7LP3JC.js → chunk-UBQ3TG3B.js} +1 -1
  108. package/dist/{chunk-SHW6KK2K.js → chunk-UU2CQFSJ.js} +2 -2
  109. package/dist/{chunk-5EQOUZYE.js → chunk-UUX6Q3CB.js} +2 -2
  110. package/dist/{chunk-SONZCJY5.js → chunk-WFSE7NRM.js} +23 -6
  111. package/dist/{chunk-DUOBFECC.js → chunk-XYNQA6YC.js} +1 -1
  112. package/dist/{chunk-KDUWFGY2.js → chunk-Y5YJLBML.js} +4 -4
  113. package/dist/{chunk-2DB7TBA7.js → chunk-ZZFYV26G.js} +2 -2
  114. package/dist/{codex-app-cards-switch-CN3G7GZW.js → codex-app-cards-switch-LTZCEFYB.js} +11 -11
  115. package/dist/{codex-desktop-launch-GFZXCDSL.js → codex-desktop-launch-DBJBV66U.js} +1 -1
  116. package/dist/{codex-session-launch-4F2UGQYE.js → codex-session-launch-6RTU73OP.js} +18 -18
  117. package/dist/{config-BLHDUDCS.js → config-YREUMA6Z.js} +3 -3
  118. package/dist/{global-upgrade-IZEC22BQ.js → global-upgrade-EYLSJXXN.js} +10 -10
  119. package/dist/{identity-3HGTEP5Q.js → identity-WIM6NVLP.js} +4 -4
  120. package/dist/{live-capabilities-AZK664T7.js → live-capabilities-SU3BEP2Z.js} +5 -5
  121. package/dist/{paths-ROMZUSEX.js → paths-XV3FXWOM.js} +3 -3
  122. package/dist/{reapply-IJYRR4YK.js → reapply-Q7QC3MWL.js} +27 -27
  123. package/dist/src/index.js +29 -28
  124. package/dist/{upgrade-lock-FGEZPX56.js → upgrade-lock-A2ALHHUG.js} +4 -4
  125. package/dist/{usage-HCURE44F.js → usage-UNEATRXG.js} +3 -3
  126. package/package.json +1 -1
@@ -3,13 +3,16 @@ import {
3
3
  loadPin,
4
4
  savePin,
5
5
  verifyRecipients
6
- } from "./chunk-T6RWREWU.js";
6
+ } from "./chunk-EV47UGDK.js";
7
+ import {
8
+ SESSION_HISTORY_RETENTION_DAYS
9
+ } from "./chunk-MHUKYYYR.js";
7
10
  import {
8
11
  ensureDaemonRunning
9
- } from "./chunk-DCHZ5JOI.js";
12
+ } from "./chunk-JJPBWBAF.js";
10
13
  import {
11
14
  hookOwnerOf
12
- } from "./chunk-TPYZFYQ5.js";
15
+ } from "./chunk-TK3XMPG2.js";
13
16
  import {
14
17
  buildTranscriptRecords,
15
18
  decodePublicKey,
@@ -20,38 +23,42 @@ import {
20
23
  recordSealerFor,
21
24
  uploadTranscriptRecords,
22
25
  wrapSessionKey
23
- } from "./chunk-SONZCJY5.js";
26
+ } from "./chunk-WFSE7NRM.js";
24
27
  import {
25
28
  external_exports
26
29
  } from "./chunk-XMSDA5WA.js";
27
30
  import {
28
31
  transcriptEnvironmentPreference,
29
32
  transcriptsEnabled
30
- } from "./chunk-RPKFXNOJ.js";
33
+ } from "./chunk-CIMFZIGP.js";
31
34
  import {
32
35
  readTranscriptPreference
33
- } from "./chunk-LIDUPDEY.js";
36
+ } from "./chunk-DYDA77FS.js";
34
37
  import {
35
38
  claudeConfigDir,
36
39
  codexHome,
37
40
  pusharyDir
38
- } from "./chunk-2J4IVUEA.js";
41
+ } from "./chunk-PZXQUV3P.js";
39
42
  import {
40
- writeJsonAtomic
41
- } from "./chunk-AUDYAVNM.js";
43
+ writeJsonAtomic,
44
+ writeJsonAtomicDurable
45
+ } from "./chunk-ABAWD6QM.js";
42
46
  import {
43
47
  getApiKey,
44
48
  getBaseUrl
45
- } from "./chunk-53D6FFVE.js";
49
+ } from "./chunk-IHT33GZN.js";
46
50
  import {
47
51
  PENDING_COMMAND_PREFIX,
52
+ SESSION_HISTORY_BACKFILL_LIMIT_BYTES,
53
+ canonicalJsonText,
54
+ isRecord,
48
55
  redactSecretsFromContent
49
- } from "./chunk-QRS7FQHE.js";
56
+ } from "./chunk-INBG473K.js";
50
57
 
51
58
  // src/transcript-registration.ts
52
- import { createHash } from "crypto";
53
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
54
- import { basename, isAbsolute, join as join4, resolve as resolve2, sep } from "path";
59
+ import { createHash as createHash4 } from "crypto";
60
+ import { readFileSync as readFileSync6, readdirSync as readdirSync2, realpathSync, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
61
+ import { basename, dirname as dirname2, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
55
62
 
56
63
  // src/claude-display-draft.ts
57
64
  import { closeSync, fstatSync, lstatSync, openSync, readFileSync, readSync, unlinkSync } from "fs";
@@ -198,18 +205,17 @@ var readDisplayDraft = (sessionId, runtime) => {
198
205
  };
199
206
 
200
207
  // src/transcript-session-key.ts
201
- import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync, writeFileSync } from "fs";
208
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync } from "fs";
202
209
  import { homedir } from "os";
203
210
  import { join as join2 } from "path";
204
211
  var sessionKeyDirectory = () => process.env.PUSHARY_SESSION_KEY_DIR?.trim() || join2(homedir(), ".pushary", "sessions");
205
- var SESSION_KEY_TTL_MS = 48 * 60 * 60 * 1e3;
212
+ var SESSION_DISPLAY_TTL_MS = 48 * 60 * 60 * 1e3;
206
213
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
207
214
  var fileFor2 = (sessionId, directory) => SAFE_SESSION_ID.test(sessionId) ? join2(directory, `${sessionId}.json`) : null;
208
215
  var loadSessionKey = (sessionId, directory = sessionKeyDirectory()) => {
209
216
  const path = fileFor2(sessionId, directory);
210
217
  if (!path || !existsSync(path)) return null;
211
218
  try {
212
- if (Date.now() - statSync(path).mtimeMs > SESSION_KEY_TTL_MS) return null;
213
219
  const raw = JSON.parse(readFileSync2(path, "utf-8"));
214
220
  const key = typeof raw.sessionKey === "string" ? Buffer.from(raw.sessionKey, "base64") : null;
215
221
  if (!key || key.length !== 32) return null;
@@ -227,33 +233,31 @@ var loadSessionKey = (sessionId, directory = sessionKeyDirectory()) => {
227
233
  };
228
234
  var saveSessionKey = (sessionId, value, directory = sessionKeyDirectory()) => {
229
235
  const path = fileFor2(sessionId, directory);
230
- if (!path) return;
236
+ if (!path) return false;
231
237
  try {
232
238
  mkdirSync(directory, { recursive: true, mode: 448 });
233
- writeFileSync(
234
- path,
235
- JSON.stringify({
236
- sessionKey: Buffer.from(value.sessionKey).toString("base64"),
237
- wrappedKey: value.wrappedKey,
238
- providerWrappedKey: value.providerWrappedKey,
239
- providerKeyId: value.providerKeyId,
240
- compressRecords: value.compressRecords,
241
- keyUploaded: value.keyUploaded
242
- }),
243
- { mode: 384 }
244
- );
239
+ writeJsonAtomicDurable(path, {
240
+ sessionKey: Buffer.from(value.sessionKey).toString("base64"),
241
+ wrappedKey: value.wrappedKey,
242
+ providerWrappedKey: value.providerWrappedKey,
243
+ providerKeyId: value.providerKeyId,
244
+ compressRecords: value.compressRecords,
245
+ keyUploaded: value.keyUploaded
246
+ }, 384);
247
+ return true;
245
248
  } catch {
249
+ return false;
246
250
  }
247
251
  };
248
- var sweepStaleSessionKeys = (now = Date.now(), dir = sessionKeyDirectory()) => {
252
+ var sweepStaleSessionDisplays = (now = Date.now(), dir = sessionKeyDirectory()) => {
249
253
  if (!existsSync(dir)) return 0;
250
254
  let removed = 0;
251
255
  try {
252
256
  for (const name of readdirSync(dir)) {
253
- if (!name.endsWith(".json") && !name.endsWith(".display")) continue;
257
+ if (!name.endsWith(".display")) continue;
254
258
  const path = join2(dir, name);
255
259
  try {
256
- if (now - statSync(path).mtimeMs <= SESSION_KEY_TTL_MS) continue;
260
+ if (now - statSync(path).mtimeMs <= SESSION_DISPLAY_TTL_MS) continue;
257
261
  rmSync(path, { force: true });
258
262
  removed += 1;
259
263
  } catch {
@@ -284,8 +288,8 @@ var startDraftPublisher = (options) => {
284
288
  if (!key?.keyUploaded) return;
285
289
  const draft = options.snapshot(key.sessionKey);
286
290
  if (!draft) return;
287
- const identity = JSON.stringify([sessionId, draft.startedAt, draft.revision]);
288
- if (identity === sent) return;
291
+ const identity2 = JSON.stringify([sessionId, draft.startedAt, draft.revision]);
292
+ if (identity2 === sent) return;
289
293
  const response = await fetch(`${options.baseUrl}/api/agent/transcript`, {
290
294
  method: "POST",
291
295
  headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json" },
@@ -297,7 +301,7 @@ var startDraftPublisher = (options) => {
297
301
  signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5e3)])
298
302
  });
299
303
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
300
- sent = identity;
304
+ sent = identity2;
301
305
  retryAt = 0;
302
306
  retryMs = 1e3;
303
307
  reportedFailure = false;
@@ -360,6 +364,11 @@ var startDisplayPublisher = (options) => {
360
364
  }).stop;
361
365
  };
362
366
 
367
+ // src/transcript-reader.ts
368
+ import { createHash as createHash3 } from "crypto";
369
+ import { join as join3 } from "path";
370
+ import { closeSync as closeSync3, fstatSync as fstatSync3, openSync as openSync3, readFileSync as readFileSync5, readSync as readSync2, statSync as statSync2 } from "fs";
371
+
363
372
  // src/transcript-source.ts
364
373
  var normalizeTranscriptLines = (lines, source) => {
365
374
  if (source === "claude") return [...lines];
@@ -376,11 +385,14 @@ var normalizeTranscriptLines = (lines, source) => {
376
385
  });
377
386
  };
378
387
  var createdDuringEpoch = (sighting, epochStartedAtMs) => sighting.birthtimeMs !== null && sighting.birthtimeMs >= epochStartedAtMs && sighting.birthtimeMs <= sighting.seenAtMs;
388
+ var SESSION_HISTORY_BACKFILL = { limitBytes: SESSION_HISTORY_BACKFILL_LIMIT_BYTES };
389
+ var MS_PER_DAY = 864e5;
379
390
  var everythingAlreadyWritten = (sighting) => ({ identity: sighting.identity, offset: sighting.sizeBytes, startedBeforeEpoch: true });
380
- var cutoffAtFirstSight = (sighting, epochStartedAtMs) => sighting.identity === null || createdDuringEpoch(sighting, epochStartedAtMs) ? { identity: sighting.identity, offset: 0, startedBeforeEpoch: false } : everythingAlreadyWritten(sighting);
391
+ var earlierConversationStart = (sighting, backfill) => backfill === null ? everythingAlreadyWritten(sighting) : { identity: sighting.identity, offset: Math.max(0, sighting.sizeBytes - backfill.limitBytes), startedBeforeEpoch: true };
392
+ var cutoffAtFirstSight = (sighting, epochStartedAtMs, backfill) => sighting.identity === null || createdDuringEpoch(sighting, epochStartedAtMs) ? { identity: sighting.identity, offset: 0, startedBeforeEpoch: false } : earlierConversationStart(sighting, backfill);
381
393
  var replacedOrShrunk = (previous, sighting) => previous.identity !== sighting.identity || sighting.sizeBytes < previous.offset;
382
- var nextTranscriptCutoff = (previous, sighting, epochStartedAtMs) => {
383
- if (!previous) return cutoffAtFirstSight(sighting, epochStartedAtMs);
394
+ var nextTranscriptCutoff = (previous, sighting, epochStartedAtMs, backfill = null) => {
395
+ if (!previous) return cutoffAtFirstSight(sighting, epochStartedAtMs, backfill);
384
396
  if (sighting.identity === null) return previous;
385
397
  if (previous.identity === null) return { ...previous, identity: sighting.identity };
386
398
  return replacedOrShrunk(previous, sighting) ? everythingAlreadyWritten(sighting) : previous;
@@ -401,19 +413,464 @@ var stampedBefore = (record, momentMs) => {
401
413
  return Number.isFinite(stamped) && stamped < momentMs;
402
414
  };
403
415
  var isCompactionSummary = (record) => record.isCompactSummary === true;
404
- var transcriptLinesSeenLive = (lines, epochStartedAtMs, startedBeforeEpoch) => lines.filter((line) => {
416
+ var oldestTranscriptLineMs = (epochStartedAtMs, backfill, retentionDays, nowMs) => backfill === null ? epochStartedAtMs : nowMs - retentionDays * MS_PER_DAY;
417
+ var dropsCompactionSummaries = (cutoff, backfill) => cutoff.startedBeforeEpoch && backfill === null;
418
+ var transcriptLinesSince = (lines, oldestLineMs, dropCompactionSummaries) => lines.filter((line) => {
405
419
  const record = parsedTranscriptRecord(line);
406
420
  if (record === null) return true;
407
- return !stampedBefore(record, epochStartedAtMs) && !(startedBeforeEpoch && isCompactionSummary(record));
421
+ return !stampedBefore(record, oldestLineMs) && !(dropCompactionSummaries && isCompactionSummary(record));
422
+ });
423
+
424
+ // src/transcript-snapshot-file.ts
425
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3 } from "fs";
426
+
427
+ // src/transcript-snapshot-source.ts
428
+ import { createHash } from "crypto";
429
+ var identity = external_exports.string().min(1).max(256);
430
+ var timestamp = external_exports.string().refine((value) => Number.isFinite(Date.parse(value)));
431
+ var geminiTool = external_exports.object({ id: identity, name: external_exports.string(), args: external_exports.record(external_exports.string(), external_exports.unknown()), result: external_exports.unknown().optional(), status: external_exports.string() });
432
+ var geminiMessage = external_exports.object({
433
+ id: identity,
434
+ type: external_exports.enum(["user", "gemini", "info", "warning", "error"]),
435
+ timestamp,
436
+ content: external_exports.unknown(),
437
+ displayContent: external_exports.unknown().optional(),
438
+ toolCalls: external_exports.array(geminiTool).optional()
439
+ });
440
+ var toolPatch = external_exports.object({ id: identity, result: external_exports.unknown().optional() });
441
+ var messagePatch = external_exports.object({ id: identity, content: external_exports.unknown().optional(), toolCalls: external_exports.array(toolPatch).optional() });
442
+ var patchSchema = messagePatch.partial().extend({ updates: external_exports.array(messagePatch).optional(), removeIds: external_exports.array(identity).optional(), orderIds: external_exports.array(identity).optional() });
443
+ var metadataSchema = external_exports.object({ sessionId: identity, kind: external_exports.enum(["main", "subagent"]).optional() });
444
+ var textParts = (value) => {
445
+ if (typeof value === "string") return [{ type: "text", text: value }];
446
+ if (!Array.isArray(value)) return [];
447
+ return value.flatMap((part) => {
448
+ if (typeof part === "string") return [{ type: "text", text: part }];
449
+ return isRecord(part) && part.thought !== true && typeof part.text === "string" ? [{ type: "text", text: part.text }] : [];
450
+ });
451
+ };
452
+ var geminiResult = (value) => {
453
+ if (typeof value === "string") return value;
454
+ if (!Array.isArray(value)) return null;
455
+ const parts = value.flatMap((part) => {
456
+ if (!isRecord(part) || part.thought === true) return [];
457
+ if (typeof part.text === "string") return [part.text];
458
+ if (isRecord(part.functionResponse) && isRecord(part.functionResponse.response)) return [JSON.stringify(part.functionResponse.response)];
459
+ return [];
460
+ });
461
+ return parts.join("\n");
462
+ };
463
+ var geminiConversationMessage = (message) => {
464
+ if (message.type !== "user" && message.type !== "gemini") return [];
465
+ const content = textParts(message.displayContent ?? message.content);
466
+ for (const tool of message.toolCalls ?? []) {
467
+ content.push({ type: "tool_use", id: tool.id, name: tool.name, input: tool.args });
468
+ if (tool.result !== void 0 && tool.result !== null) content.push({ type: "tool_result", tool_use_id: tool.id, content: geminiResult(tool.result), is_error: tool.status === "error" });
469
+ }
470
+ return [{ id: message.id, role: message.type === "user" ? "user" : "assistant", timestamp: message.timestamp, content }];
471
+ };
472
+ var applyMessagePatch = (messages, patch) => {
473
+ const previous = messages.get(patch.id);
474
+ if (!previous) return;
475
+ const updates = new Map(patch.toolCalls?.map((tool) => [tool.id, tool]));
476
+ const toolCalls = previous.toolCalls?.map((tool) => {
477
+ const update = updates.get(tool.id);
478
+ return update && Object.hasOwn(update, "result") ? { ...tool, result: update.result } : tool;
479
+ });
480
+ messages.set(patch.id, { ...previous, ...Object.hasOwn(patch, "content") ? { content: patch.content } : {}, toolCalls });
481
+ };
482
+ var applyGeminiPatch = (messages, value) => {
483
+ const patch = patchSchema.parse(value);
484
+ if (patch.id) applyMessagePatch(messages, { ...patch, id: patch.id });
485
+ for (const update of patch.updates ?? []) applyMessagePatch(messages, update);
486
+ for (const id of patch.removeIds ?? []) messages.delete(id);
487
+ for (const id of patch.orderIds ?? []) {
488
+ const message = messages.get(id);
489
+ if (message) {
490
+ messages.delete(id);
491
+ messages.set(id, message);
492
+ }
493
+ }
494
+ };
495
+ var replaceMessages = (messages, value) => {
496
+ const replacement = external_exports.array(geminiMessage).parse(value);
497
+ messages.clear();
498
+ for (const message of replacement) messages.set(message.id, message);
499
+ };
500
+ var rewindMessages = (messages, id) => {
501
+ if (!messages.has(id)) {
502
+ messages.clear();
503
+ return;
504
+ }
505
+ let remove = false;
506
+ for (const key of messages.keys()) {
507
+ if (key === id) remove = true;
508
+ if (remove) messages.delete(key);
509
+ }
510
+ };
511
+ var parseGeminiSnapshot = (text, sessionId) => {
512
+ let records;
513
+ try {
514
+ records = [JSON.parse(text)];
515
+ } catch {
516
+ records = text.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
517
+ }
518
+ const messages = /* @__PURE__ */ new Map();
519
+ let identified = false;
520
+ for (const record of records) {
521
+ if (!isRecord(record)) throw new Error("Invalid Gemini history record");
522
+ if ("sessionId" in record) {
523
+ const metadata = metadataSchema.parse(record);
524
+ if (metadata.sessionId !== sessionId || metadata.kind === "subagent") throw new Error("Gemini history identity mismatch");
525
+ identified = true;
526
+ if ("messages" in record) replaceMessages(messages, record.messages);
527
+ } else if ("$rewindTo" in record) rewindMessages(messages, identity.parse(record.$rewindTo));
528
+ else if ("$patch" in record) applyGeminiPatch(messages, record.$patch);
529
+ else if ("$set" in record) {
530
+ if (!isRecord(record.$set)) throw new Error("Invalid Gemini checkpoint");
531
+ if ("sessionId" in record.$set && record.$set.sessionId !== sessionId) throw new Error("Gemini checkpoint identity mismatch");
532
+ if (record.$set.kind === "subagent") throw new Error("Gemini subagent history is not a main session");
533
+ if ("messages" in record.$set) replaceMessages(messages, record.$set.messages);
534
+ } else {
535
+ const message = geminiMessage.parse(record);
536
+ messages.set(message.id, message);
537
+ }
538
+ }
539
+ if (!identified) throw new Error("Gemini history metadata unavailable");
540
+ return [...messages.values()].flatMap(geminiConversationMessage);
541
+ };
542
+ var cursorBlock = external_exports.discriminatedUnion("type", [
543
+ external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }),
544
+ external_exports.object({ type: external_exports.literal("tool_use"), name: external_exports.string(), input: external_exports.record(external_exports.string(), external_exports.unknown()) })
545
+ ]);
546
+ var cursorMessage = external_exports.object({ role: external_exports.enum(["user", "assistant"]), message: external_exports.object({ content: external_exports.array(cursorBlock) }) });
547
+ var parseCursorSnapshot = (text) => {
548
+ const messages = [];
549
+ const occurrences = /* @__PURE__ */ new Map();
550
+ for (const line of text.split("\n").filter((line2) => line2.trim())) {
551
+ const record = JSON.parse(line);
552
+ if (isRecord(record) && (record.type === "metadata" || record.type === "turn_ended")) continue;
553
+ const message = cursorMessage.parse(record);
554
+ const fingerprint = createHash("sha256").update(canonicalJsonText(message)).digest("hex");
555
+ const occurrence = occurrences.get(fingerprint) ?? 0;
556
+ occurrences.set(fingerprint, occurrence + 1);
557
+ messages.push({ id: `cursor:${fingerprint}:${occurrence}`, role: message.role, content: message.message.content });
558
+ }
559
+ return messages;
560
+ };
561
+ var historyBlock = external_exports.discriminatedUnion("type", [
562
+ external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }),
563
+ external_exports.object({ type: external_exports.literal("tool_use"), id: identity, name: external_exports.string(), input: external_exports.record(external_exports.string(), external_exports.unknown()) }),
564
+ external_exports.object({ type: external_exports.literal("tool_result"), tool_use_id: identity, content: external_exports.unknown(), is_error: external_exports.boolean().optional() })
565
+ ]);
566
+ var hermesSnapshot = external_exports.object({
567
+ version: external_exports.literal(1),
568
+ sessionId: identity,
569
+ completed: external_exports.boolean().default(false),
570
+ updatedAtMs: external_exports.number().int().positive(),
571
+ error: external_exports.string().optional(),
572
+ messages: external_exports.array(external_exports.object({ id: identity, role: external_exports.enum(["user", "assistant"]), timestamp, content: external_exports.array(historyBlock) })).max(5e3)
573
+ });
574
+ var TranscriptSourceUnavailable = class extends Error {
575
+ };
576
+ var parseHermesSnapshot = (text, sessionId, nowMs) => {
577
+ const snapshot = hermesSnapshot.parse(JSON.parse(text));
578
+ if (snapshot.sessionId !== sessionId) throw new Error("Hermes history identity mismatch");
579
+ if (snapshot.error || !snapshot.completed && nowMs - snapshot.updatedAtMs > 18e4 || snapshot.updatedAtMs > nowMs + 5e3) throw new TranscriptSourceUnavailable("Hermes history exporter is unavailable");
580
+ return snapshot.messages;
581
+ };
582
+
583
+ // src/transcript-snapshot-file.ts
584
+ var MAXIMUM_SNAPSHOT_MESSAGES = 5e3;
585
+ var readTranscriptSnapshot = (path, source, sessionId) => {
586
+ const descriptor = openSync2(path, "r");
587
+ try {
588
+ const before = fstatSync2(descriptor);
589
+ if (!before.isFile() || before.size === 0 || before.size > SESSION_HISTORY_BACKFILL_LIMIT_BYTES) throw new Error("History snapshot is empty or exceeds its size limit");
590
+ const text = readFileSync3(descriptor, "utf8");
591
+ const after = fstatSync2(descriptor);
592
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs) throw new Error("History snapshot changed while reading");
593
+ let messages;
594
+ switch (source) {
595
+ case "gemini":
596
+ messages = parseGeminiSnapshot(text, sessionId);
597
+ break;
598
+ case "cursor":
599
+ messages = parseCursorSnapshot(text);
600
+ break;
601
+ case "hermes":
602
+ messages = parseHermesSnapshot(text, sessionId, Date.now());
603
+ break;
604
+ }
605
+ if (messages.length > MAXIMUM_SNAPSHOT_MESSAGES) throw new Error("History snapshot exceeds its message limit");
606
+ return messages;
607
+ } finally {
608
+ closeSync2(descriptor);
609
+ }
610
+ };
611
+
612
+ // src/transcript-snapshot-journal.ts
613
+ import { lstatSync as lstatSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
614
+ import { dirname } from "path";
615
+
616
+ // src/transcript-projection.ts
617
+ import { createHash as createHash2 } from "crypto";
618
+ var projectTranscriptSnapshot = (previous, messages) => {
619
+ const revision = (previous?.revision ?? 0) + 1;
620
+ const entries = /* @__PURE__ */ Object.create(null);
621
+ const lines = [];
622
+ for (const [order, message] of messages.entries()) {
623
+ if (!message.id || message.id.length > 256 || Object.hasOwn(entries, message.id)) throw new Error("Invalid or duplicate history message identity");
624
+ const record = { timestamp: message.timestamp, observedAtMs: message.observedAtMs, historyMessage: { role: message.role, content: message.content } };
625
+ const digest = createHash2("sha256").update(JSON.stringify(record)).digest("hex");
626
+ entries[message.id] = { digest, order };
627
+ const earlier = previous?.entries[message.id];
628
+ if (earlier?.digest === digest && earlier.order === order) continue;
629
+ lines.push(JSON.stringify({ ...record, historySource: { id: message.id, order, removed: false }, historyRevision: revision }));
630
+ }
631
+ for (const [id, entry] of Object.entries(previous?.entries ?? {})) {
632
+ if (Object.hasOwn(entries, id)) continue;
633
+ lines.push(JSON.stringify({ historySource: { id, order: entry.order, removed: true }, historyRevision: revision }));
634
+ }
635
+ return { lines, next: { revision: lines.length ? revision : previous?.revision ?? 0, entries } };
636
+ };
637
+
638
+ // src/transcript-snapshot-journal.ts
639
+ var MAXIMUM_JOURNAL_BYTES = 64 * 1024 * 1024;
640
+ var projectionSchema = external_exports.object({
641
+ revision: external_exports.number().int().nonnegative(),
642
+ entries: external_exports.record(external_exports.string(), external_exports.object({ digest: external_exports.string().regex(/^[a-f0-9]{64}$/), order: external_exports.number().int().nonnegative() }))
643
+ });
644
+ var journalSchema = external_exports.object({
645
+ version: external_exports.literal(1),
646
+ binding: external_exports.string(),
647
+ epochStartedAtMs: external_exports.number(),
648
+ initialized: external_exports.boolean(),
649
+ observedAtMs: external_exports.record(external_exports.string(), external_exports.number().nonnegative()).default({}),
650
+ excludedIds: external_exports.array(external_exports.string()),
651
+ projection: projectionSchema.nullable(),
652
+ pending: external_exports.object({ lines: external_exports.array(external_exports.string()), next: projectionSchema }).nullable()
653
+ });
654
+ var TranscriptSnapshotJournal = class {
655
+ constructor(path, binding, epochStartedAtMs) {
656
+ this.path = path;
657
+ const info = lstatSync2(path, { throwIfNoEntry: false });
658
+ if (!info) {
659
+ this.current = { version: 1, binding, epochStartedAtMs, initialized: false, observedAtMs: {}, excludedIds: [], projection: null, pending: null };
660
+ return;
661
+ }
662
+ if (!info.isFile() || info.nlink !== 1 || info.size > MAXIMUM_JOURNAL_BYTES || process.getuid && (info.uid !== process.getuid() || (info.mode & 511) !== 384)) throw new Error("Unsafe history checkpoint");
663
+ const saved = journalSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
664
+ if (saved.binding !== binding) throw new Error("History checkpoint belongs to another source");
665
+ this.current = saved.epochStartedAtMs === epochStartedAtMs ? saved : { ...saved, epochStartedAtMs, initialized: false, observedAtMs: {}, pending: null, excludedIds: [], projection: { revision: Math.max(saved.projection?.revision ?? 0, saved.pending?.next.revision ?? 0), entries: {} } };
666
+ }
667
+ current;
668
+ prepare(messages, oldestMessageMs, nowMs = Date.now()) {
669
+ const pending = this.pendingLinesSince(oldestMessageMs);
670
+ if (pending) return pending;
671
+ const identities = new Set(messages.map((message) => message.id));
672
+ const excluded = new Set(this.current.excludedIds.filter((id) => identities.has(id)));
673
+ if (!this.current.initialized) {
674
+ for (const message of messages) if (!message.timestamp) excluded.add(message.id);
675
+ }
676
+ const observedAtMs = Object.fromEntries(Object.entries(this.current.observedAtMs).filter(([id]) => identities.has(id)));
677
+ for (const message of messages) {
678
+ if (!message.timestamp && !excluded.has(message.id) && !Object.hasOwn(observedAtMs, message.id)) observedAtMs[message.id] = nowMs;
679
+ }
680
+ const allowed = messages.filter((message) => !excluded.has(message.id) && (message.timestamp ? Date.parse(message.timestamp) : observedAtMs[message.id] ?? 0) >= oldestMessageMs).map((message) => message.timestamp ? message : { ...message, observedAtMs: observedAtMs[message.id] });
681
+ const projected = projectTranscriptSnapshot(this.current.projection, allowed);
682
+ if (projected.lines.length === 0) {
683
+ const metadataChanged = excluded.size !== this.current.excludedIds.length || Object.keys(observedAtMs).length !== Object.keys(this.current.observedAtMs).length;
684
+ if (!this.current.initialized || metadataChanged) this.store({ ...this.current, initialized: true, observedAtMs, excludedIds: [...excluded], projection: projected.next, pending: null });
685
+ return [];
686
+ }
687
+ this.store({ ...this.current, initialized: true, observedAtMs, excludedIds: [...excluded], pending: { lines: [...projected.lines], next: projected.next } });
688
+ return projected.lines;
689
+ }
690
+ pendingLinesSince(oldestMessageMs) {
691
+ const pending = this.current.pending;
692
+ if (!pending) return null;
693
+ const lines = pending.lines.filter((line) => {
694
+ const record = JSON.parse(line);
695
+ if (!record || typeof record !== "object") throw new Error("Invalid pending history record");
696
+ if ("timestamp" in record) return typeof record.timestamp === "string" && Date.parse(record.timestamp) >= oldestMessageMs;
697
+ return !("observedAtMs" in record) || typeof record.observedAtMs === "number" && record.observedAtMs >= oldestMessageMs;
698
+ });
699
+ if (lines.length !== pending.lines.length) this.store({ ...this.current, pending: { ...pending, lines } });
700
+ return lines;
701
+ }
702
+ settle() {
703
+ if (!this.current.pending) return;
704
+ this.store({ ...this.current, projection: this.current.pending.next, pending: null });
705
+ }
706
+ store(next) {
707
+ if (Buffer.byteLength(JSON.stringify(next)) > MAXIMUM_JOURNAL_BYTES) throw new Error("History checkpoint exceeds its size limit");
708
+ mkdirSync2(dirname(this.path), { recursive: true, mode: 448 });
709
+ writeJsonAtomicDurable(this.path, next, 384);
710
+ this.current = journalSchema.parse(next);
711
+ }
712
+ };
713
+
714
+ // src/transcript-reader.ts
715
+ var readPositionsKeptInMemory = { load: () => /* @__PURE__ */ new Map(), save: () => {
716
+ } };
717
+ var startTranscriptCaptureEpoch = (startedAtMs, positions = readPositionsKeptInMemory, backfill = null) => ({ startedAtMs, cutoffs: new Map(positions.load()), positions, backfill });
718
+ var recordedBirthtime = (info) => process.platform === "darwin" && info.birthtimeMs > 0 ? info.birthtimeMs : null;
719
+ var sightingOf = (info, seenAtMs) => info ? { identity: `${info.dev}:${info.ino}`, sizeBytes: info.size, birthtimeMs: recordedBirthtime(info), seenAtMs } : { identity: null, sizeBytes: 0, birthtimeMs: null, seenAtMs };
720
+ var storeCutoff = (epoch, path, cutoff) => {
721
+ epoch.cutoffs.set(path, cutoff);
722
+ epoch.positions.save(epoch.cutoffs);
723
+ };
724
+ var recordSighting = (epoch, path, sighting) => {
725
+ const previous = epoch.cutoffs.get(path);
726
+ const cutoff = nextTranscriptCutoff(previous, sighting, epoch.startedAtMs, epoch.backfill);
727
+ if (cutoff !== previous) storeCutoff(epoch, path, cutoff);
728
+ return cutoff;
729
+ };
730
+ var recordSettledPosition = (epoch, path, readFrom, settledThroughOffset) => {
731
+ const current = epoch.cutoffs.get(path);
732
+ const advanced = advancedTranscriptCutoff(current, readFrom, settledThroughOffset);
733
+ if (advanced !== void 0 && advanced !== current) storeCutoff(epoch, path, advanced);
734
+ };
735
+ var forgetUnregisteredTranscripts = (epoch, registeredPaths) => {
736
+ const unregistered = [...epoch.cutoffs.keys()].filter((path) => !registeredPaths.has(path));
737
+ if (unregistered.length === 0) return;
738
+ for (const path of unregistered) epoch.cutoffs.delete(path);
739
+ epoch.positions.save(epoch.cutoffs);
740
+ };
741
+ var statOrNull = (path) => {
742
+ try {
743
+ return statSync2(path);
744
+ } catch {
745
+ return null;
746
+ }
747
+ };
748
+ var sightTranscript = (epoch, path, seenAtMs) => recordSighting(epoch, path, sightingOf(statOrNull(path), seenAtMs));
749
+ var LINE_FEED = 10;
750
+ var MAXIMUM_TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
751
+ var nothingUnread = (cutoff) => ({ lines: [], cutoff, completeThroughOffset: cutoff.offset, reachedEnd: true });
752
+ var readThroughALineEnd = (descriptor, offset, unread) => {
753
+ let window = Math.min(unread, MAXIMUM_TRANSCRIPT_READ_BYTES);
754
+ for (; ; ) {
755
+ const buffer = Buffer.alloc(window);
756
+ const received = buffer.subarray(0, readSync2(descriptor, buffer, 0, window, offset));
757
+ if (window >= unread || received.length < window || received.includes(LINE_FEED)) return received;
758
+ window = Math.min(unread, window * 2);
759
+ }
760
+ };
761
+ var readLinesSinceCutoff = (epoch, path, seenAtMs) => {
762
+ let descriptor;
763
+ try {
764
+ descriptor = openSync3(path, "r");
765
+ } catch {
766
+ return nothingUnread(sightTranscript(epoch, path, seenAtMs));
767
+ }
768
+ try {
769
+ const info = fstatSync3(descriptor);
770
+ const cutoff = recordSighting(epoch, path, sightingOf(info, seenAtMs));
771
+ const unread = info.size - cutoff.offset;
772
+ if (unread <= 0) return nothingUnread(cutoff);
773
+ const received = readThroughALineEnd(descriptor, cutoff.offset, unread);
774
+ const completeBytes = received.lastIndexOf(LINE_FEED) + 1;
775
+ const reachedEnd = received.length >= unread;
776
+ return {
777
+ lines: (reachedEnd ? received : received.subarray(0, completeBytes)).toString("utf8").split("\n"),
778
+ cutoff,
779
+ completeThroughOffset: cutoff.offset + completeBytes,
780
+ reachedEnd
781
+ };
782
+ } finally {
783
+ closeSync3(descriptor);
784
+ }
785
+ };
786
+ var linesSeenDuringEpoch = (epoch, path, source, seenAtMs, retentionDays) => {
787
+ const read2 = readLinesSinceCutoff(epoch, path, seenAtMs);
788
+ const oldestLineMs = oldestTranscriptLineMs(epoch.startedAtMs, epoch.backfill, retentionDays, seenAtMs);
789
+ return {
790
+ ...read2,
791
+ lines: transcriptLinesSince(normalizeTranscriptLines(read2.lines, source), oldestLineMs, dropsCompactionSummaries(read2.cutoff, epoch.backfill))
792
+ };
793
+ };
794
+ var sourceVersion = (path) => {
795
+ const info = statOrNull(path);
796
+ if (!info?.isFile()) throw new TranscriptSourceUnavailable("Conversation source unavailable");
797
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}`;
798
+ };
799
+ var appendOnlyReader = (path, source, epoch) => ({
800
+ requiresRevisions: false,
801
+ read(retentionDays, nowMs) {
802
+ const version = sourceVersion(path);
803
+ const read2 = epoch ? linesSeenDuringEpoch(epoch, path, source, nowMs, retentionDays) : null;
804
+ const lines = read2?.lines ?? normalizeTranscriptLines(readFileSync5(path, "utf8").split("\n"), source);
805
+ return {
806
+ lines,
807
+ acknowledge() {
808
+ if (epoch && read2) recordSettledPosition(epoch, path, read2.cutoff, read2.completeThroughOffset);
809
+ if ((read2?.reachedEnd ?? true) && sourceVersion(path) === version) return "caught_up";
810
+ return !read2 || read2.completeThroughOffset > read2.cutoff.offset ? "more" : "unchanged";
811
+ }
812
+ };
813
+ }
408
814
  });
815
+ var snapshotReader = (options, source) => {
816
+ const { path, epoch } = options;
817
+ const binding = createHash3("sha256").update(JSON.stringify([options.baseUrl, options.apiKey, options.machineId, options.sessionId, source, path])).digest("hex");
818
+ const journalPath = join3(options.keyDirectory, "snapshots", binding + ".json");
819
+ const epochStart = epoch?.startedAtMs ?? 0;
820
+ const backfill = epoch ? epoch.backfill : SESSION_HISTORY_BACKFILL;
821
+ let journal;
822
+ return {
823
+ requiresRevisions: true,
824
+ read(retentionDays, nowMs) {
825
+ const checkpoint = journal ??= new TranscriptSnapshotJournal(journalPath, binding, backfill ? 0 : epochStart);
826
+ const oldestMessage = oldestTranscriptLineMs(epochStart, backfill, retentionDays, nowMs);
827
+ const pending = checkpoint.pendingLinesSince(oldestMessage);
828
+ const version = pending === null ? sourceVersion(path) : null;
829
+ const lines = pending ?? checkpoint.prepare(readTranscriptSnapshot(path, source, options.sessionId), oldestMessage, nowMs);
830
+ return {
831
+ lines,
832
+ acknowledge() {
833
+ checkpoint.settle();
834
+ return version === null || sourceVersion(path) !== version ? "more" : "caught_up";
835
+ }
836
+ };
837
+ }
838
+ };
839
+ };
840
+ var createTranscriptReader = (options) => options.source === "claude" || options.source === "codex" ? appendOnlyReader(options.path, options.source, options.epoch) : snapshotReader(options, options.source);
841
+
842
+ // src/transcript-health.ts
843
+ var HEALTH_REFRESH_MS = 6e4;
844
+ var transcriptHealthReporter = (target) => {
845
+ let reported;
846
+ let reportedAt = 0;
847
+ let warned = false;
848
+ return async (state) => {
849
+ if (!target.machineId || state === reported && Date.now() - reportedAt < HEALTH_REFRESH_MS) return;
850
+ try {
851
+ const response = await fetch(`${target.baseUrl}/api/agent/transcript/status`, {
852
+ method: "POST",
853
+ headers: { Authorization: `Bearer ${target.apiKey}`, "Content-Type": "application/json" },
854
+ body: JSON.stringify({ sessionId: target.sessionId, machineId: target.machineId, captureState: state }),
855
+ signal: AbortSignal.timeout(2e3)
856
+ });
857
+ if (!response.ok) throw new Error("History status rejected");
858
+ reported = state;
859
+ reportedAt = Date.now();
860
+ warned = false;
861
+ } catch {
862
+ if (!warned) process.stderr.write("[pushary] History sync status could not reach the server.\n");
863
+ warned = true;
864
+ }
865
+ };
866
+ };
409
867
 
410
868
  // src/transcript-sync.ts
411
- import { join as join3, resolve } from "path";
412
- import { closeSync as closeSync2, existsSync as existsSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3, readSync as readSync2, statSync as statSync2 } from "fs";
869
+ import { join as join4, resolve } from "path";
413
870
  var claudeTranscriptPath = (cwd, sessionId) => {
414
871
  const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
415
872
  const configDir = claudeConfigDir();
416
- return join3(configDir, "projects", projectId, `${sessionId}.jsonl`);
873
+ return join4(configDir, "projects", projectId, `${sessionId}.jsonl`);
417
874
  };
418
875
  var createSyncState = () => ({
419
876
  sessionKey: null,
@@ -425,7 +882,9 @@ var createSyncState = () => ({
425
882
  keyUploaded: false,
426
883
  keyRejected: false,
427
884
  keyUnavailable: false,
428
- unsettled: 0
885
+ rejectedRecords: 0,
886
+ unsettled: 0,
887
+ retentionDays: null
429
888
  });
430
889
  var withSessionKey = async (state, uploader) => {
431
890
  if (state.sessionKey || state.keyRejected) return state;
@@ -438,7 +897,8 @@ var withSessionKey = async (state, uploader) => {
438
897
  wrappedKey: wrapSessionKey(sessionKey, decodePublicKey(recipients.phonePublicKey)),
439
898
  providerWrappedKey: recipients.provider ? wrapSessionKey(sessionKey, decodePublicKey(recipients.provider.publicKey)) : state.providerWrappedKey,
440
899
  providerKeyId: recipients.provider ? recipients.provider.keyId : state.providerKeyId,
441
- compressRecords: recipients.phoneReadsCompressedRecords
900
+ compressRecords: recipients.phoneReadsCompressedRecords,
901
+ retentionDays: recipients.retentionDays
442
902
  };
443
903
  };
444
904
  var syncTick = async (lines, state, uploader, sessionId, machineId) => {
@@ -491,7 +951,9 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
491
951
  keyUploaded,
492
952
  keyRejected,
493
953
  keyUnavailable,
494
- unsettled: built.records.filter((record) => !seen.has(record.localId)).length
954
+ rejectedRecords: state.rejectedRecords + rejected,
955
+ unsettled: built.records.filter((record) => !seen.has(record.localId)).length,
956
+ retentionDays: keyed.retentionDays
495
957
  };
496
958
  };
497
959
  var MAX_RECORDS_PER_BATCH = 200;
@@ -513,99 +975,34 @@ var chunkRecords = (records) => {
513
975
  if (current.length > 0) chunks.push(current);
514
976
  return chunks;
515
977
  };
516
- var readLines = (path) => {
517
- try {
518
- return existsSync2(path) ? readFileSync3(path, "utf-8").split("\n") : [];
519
- } catch {
520
- return [];
521
- }
522
- };
523
978
  var IDLE_CEILING_MS = 6e4;
524
- var recordedBirthtime = (info) => process.platform === "darwin" && info.birthtimeMs > 0 ? info.birthtimeMs : null;
525
- var sightingOf = (info, seenAtMs) => info ? { identity: `${info.dev}:${info.ino}`, sizeBytes: info.size, birthtimeMs: recordedBirthtime(info), seenAtMs } : { identity: null, sizeBytes: 0, birthtimeMs: null, seenAtMs };
526
- var storeCutoff = (epoch, path, cutoff) => {
527
- epoch.cutoffs.set(path, cutoff);
528
- epoch.positions.save(epoch.cutoffs);
529
- };
530
- var recordSighting = (epoch, path, sighting) => {
531
- const previous = epoch.cutoffs.get(path);
532
- const cutoff = nextTranscriptCutoff(previous, sighting, epoch.startedAtMs);
533
- if (cutoff !== previous) storeCutoff(epoch, path, cutoff);
534
- return cutoff;
535
- };
536
- var recordSettledPosition = (epoch, path, readFrom, settledThroughOffset) => {
537
- const current = epoch.cutoffs.get(path);
538
- const advanced = advancedTranscriptCutoff(current, readFrom, settledThroughOffset);
539
- if (advanced !== void 0 && advanced !== current) storeCutoff(epoch, path, advanced);
540
- };
541
- var forgetUnregisteredTranscripts = (epoch, registeredPaths) => {
542
- const unregistered = [...epoch.cutoffs.keys()].filter((path) => !registeredPaths.has(path));
543
- if (unregistered.length === 0) return;
544
- for (const path of unregistered) epoch.cutoffs.delete(path);
545
- epoch.positions.save(epoch.cutoffs);
546
- };
547
- var statOrNull = (path) => {
548
- try {
549
- return statSync2(path);
550
- } catch {
551
- return null;
552
- }
553
- };
554
- var sightTranscript = (epoch, path, seenAtMs) => recordSighting(epoch, path, sightingOf(statOrNull(path), seenAtMs));
555
- var LINE_FEED = 10;
556
- var MAXIMUM_TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
979
+ var RECIPIENT_REFRESH_MS = 6e4;
557
980
  var CATCH_UP_DELAY_MS = 50;
558
981
  var STALLED_BACKOFF_FACTOR = 4;
559
- var nothingUnread = (cutoff) => ({ lines: [], cutoff, completeThroughOffset: cutoff.offset, reachedEnd: true });
560
- var readThroughALineEnd = (descriptor, offset, unread) => {
561
- let window = Math.min(unread, MAXIMUM_TRANSCRIPT_READ_BYTES);
562
- for (; ; ) {
563
- const buffer = Buffer.alloc(window);
564
- const received = buffer.subarray(0, readSync2(descriptor, buffer, 0, window, offset));
565
- if (window >= unread || received.length < window || received.includes(LINE_FEED)) return received;
566
- window = Math.min(unread, window * 2);
567
- }
568
- };
569
- var readLinesSinceCutoff = (epoch, path, seenAtMs) => {
570
- let descriptor;
571
- try {
572
- descriptor = openSync2(path, "r");
573
- } catch {
574
- return nothingUnread(sightTranscript(epoch, path, seenAtMs));
575
- }
576
- try {
577
- const info = fstatSync2(descriptor);
578
- const cutoff = recordSighting(epoch, path, sightingOf(info, seenAtMs));
579
- const unread = info.size - cutoff.offset;
580
- if (unread <= 0) return nothingUnread(cutoff);
581
- const received = readThroughALineEnd(descriptor, cutoff.offset, unread);
582
- const completeBytes = received.lastIndexOf(LINE_FEED) + 1;
583
- const reachedEnd = received.length >= unread;
584
- return {
585
- lines: (reachedEnd ? received : received.subarray(0, completeBytes)).toString("utf8").split("\n"),
586
- cutoff,
587
- completeThroughOffset: cutoff.offset + completeBytes,
588
- reachedEnd
589
- };
590
- } finally {
591
- closeSync2(descriptor);
592
- }
593
- };
594
- var linesSeenDuringEpoch = (epoch, path, source, seenAtMs) => {
595
- const read2 = readLinesSinceCutoff(epoch, path, seenAtMs);
596
- return { ...read2, lines: transcriptLinesSeenLive(normalizeTranscriptLines(read2.lines, source), epoch.startedAtMs, read2.cutoff.startedBeforeEpoch) };
597
- };
598
982
  var nothingLeftToUpload = (state) => state.sessionKey !== null && !state.keyRejected && state.unsettled === 0;
599
983
  var uploadStalled = (before, after) => after.sessionKey !== null && !after.keyRejected && after.unsettled > 0 && after.seen.size === before.seen.size;
984
+ var captureHealth = (state, stalled, catchingUp) => {
985
+ if (state.keyRejected || state.keyUnavailable || state.rejectedRecords > 0 || stalled) return "capture_failed";
986
+ return catchingUp ? "catching_up" : "capturing";
987
+ };
600
988
  var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
601
989
  var startTranscriptSync = (opts) => {
990
+ const reportHealth = transcriptHealthReporter({ ...opts, baseUrl: opts.baseUrl ?? getBaseUrl() });
991
+ let recipientFailure = "capture_failed";
602
992
  let warnedAboutRecipients = false;
993
+ let verifiedRecipients = null;
994
+ let recipientsVerifiedAt = 0;
603
995
  const fetchPinnedRecipients = async () => {
604
- const offer = await fetchTranscriptRecipients(opts.apiKey, opts.baseUrl);
996
+ if (verifiedRecipients && Date.now() - recipientsVerifiedAt < RECIPIENT_REFRESH_MS) return verifiedRecipients;
997
+ verifiedRecipients = null;
998
+ const offer = await fetchTranscriptRecipients(opts.apiKey, opts.baseUrl, (reason) => {
999
+ recipientFailure = reason;
1000
+ });
605
1001
  if (!offer) return null;
606
1002
  const verdict = verifyRecipients(offer, loadPin(opts.storage?.recipientPinFile));
607
1003
  if (verdict.kind === "first-use" && !savePin(verdict.pin, opts.storage?.recipientPinFile)) return null;
608
1004
  if (verdict.kind === "changed") {
1005
+ recipientFailure = "phone_key_changed";
609
1006
  if (!warnedAboutRecipients) {
610
1007
  warnedAboutRecipients = true;
611
1008
  process.stderr.write(`${changedRecipientsMessage(verdict, offer)}
@@ -613,6 +1010,8 @@ var startTranscriptSync = (opts) => {
613
1010
  }
614
1011
  return null;
615
1012
  }
1013
+ verifiedRecipients = offer;
1014
+ recipientsVerifiedAt = Date.now();
616
1015
  return offer;
617
1016
  };
618
1017
  const uploader = {
@@ -620,16 +1019,15 @@ var startTranscriptSync = (opts) => {
620
1019
  uploadRecords: (params) => (opts.enabled ? opts.enabled() : transcriptsEnabled()) ? uploadTranscriptRecords(opts.apiKey, params, opts.baseUrl) : Promise.resolve({ ok: false, keyRegistered: false, keyConflict: false, keyUnavailable: false, settledIds: null, rejectedIds: [] })
621
1020
  };
622
1021
  const path = opts.path ?? claudeTranscriptPath(opts.cwd, opts.sessionId);
1022
+ const reader = createTranscriptReader({ ...opts, source: opts.source ?? "claude", path, baseUrl: opts.baseUrl ?? getBaseUrl(), keyDirectory: opts.storage?.sessionKeyDirectory ?? sessionKeyDirectory() });
623
1023
  const persisted = loadSessionKey(opts.sessionId, opts.storage?.sessionKeyDirectory);
624
1024
  let state = persisted ? { ...createSyncState(), ...persisted } : createSyncState();
625
- sweepStaleSessionKeys(Date.now(), opts.storage?.sessionKeyDirectory);
1025
+ sweepStaleSessionDisplays(Date.now(), opts.storage?.sessionKeyDirectory);
626
1026
  let persistedKey = persisted?.sessionKey ?? null;
627
1027
  let persistedUploaded = persisted?.keyUploaded ?? false;
628
1028
  const persistSessionKey = () => {
629
1029
  if (!state.sessionKey || state.sessionKey === persistedKey && state.keyUploaded === persistedUploaded) return;
630
- persistedKey = state.sessionKey;
631
- persistedUploaded = state.keyUploaded;
632
- saveSessionKey(opts.sessionId, {
1030
+ const saved = saveSessionKey(opts.sessionId, {
633
1031
  sessionKey: state.sessionKey,
634
1032
  wrappedKey: state.wrappedKey,
635
1033
  providerWrappedKey: state.providerWrappedKey,
@@ -637,6 +1035,9 @@ var startTranscriptSync = (opts) => {
637
1035
  compressRecords: state.compressRecords,
638
1036
  keyUploaded: state.keyUploaded
639
1037
  }, opts.storage?.sessionKeyDirectory);
1038
+ if (!saved) throw new Error("Could not persist transcript encryption key");
1039
+ persistedKey = state.sessionKey;
1040
+ persistedUploaded = state.keyUploaded;
640
1041
  };
641
1042
  let running = true;
642
1043
  let inFlight = false;
@@ -649,26 +1050,42 @@ var startTranscriptSync = (opts) => {
649
1050
  try {
650
1051
  const ending = opts.ending?.() === true;
651
1052
  if (state.keyRejected) {
1053
+ await reportHealth("capture_failed");
652
1054
  if (ending) opts.onDrained?.();
653
1055
  return;
654
1056
  }
655
- state = await withSessionKey(state, uploader);
656
- if (!state.sessionKey) return;
1057
+ const recipients = await fetchPinnedRecipients();
1058
+ if (!recipients) {
1059
+ await reportHealth(recipientFailure);
1060
+ return;
1061
+ }
1062
+ if (reader.requiresRevisions && recipients.phoneReadsTranscriptRevisions !== true) {
1063
+ await reportHealth("phone_upgrade_required");
1064
+ return;
1065
+ }
1066
+ state = await withSessionKey({ ...state, retentionDays: recipients.retentionDays }, uploader);
1067
+ if (!state.sessionKey) {
1068
+ await reportHealth(recipientFailure);
1069
+ return;
1070
+ }
657
1071
  persistSessionKey();
658
1072
  const previous = state;
659
- const modified = existsSync2(path) ? statSync2(path).mtimeMs : null;
660
1073
  const source = opts.source ?? "claude";
661
- const read2 = opts.epoch ? linesSeenDuringEpoch(opts.epoch, path, source, Date.now()) : null;
662
- const lines = read2?.lines ?? normalizeTranscriptLines(readLines(path), source);
663
- state = await syncTick(lines, state, uploader, opts.sessionId, opts.machineId);
1074
+ const batch = reader.read(state.retentionDays ?? SESSION_HISTORY_RETENTION_DAYS.standard, Date.now());
1075
+ state = await syncTick(batch.lines, state, uploader, opts.sessionId, opts.machineId);
664
1076
  stalled = uploadStalled(previous, state);
665
1077
  persistSessionKey();
666
1078
  const settled = nothingLeftToUpload(state);
667
- if (opts.epoch && read2 && settled) recordSettledPosition(opts.epoch, path, read2.cutoff, read2.completeThroughOffset);
668
- catchingUp = settled && read2 !== null && !read2.reachedEnd && read2.completeThroughOffset > read2.cutoff.offset;
669
- const readEverything = read2?.reachedEnd ?? true;
670
- if (ending && opts.ending?.() && existsSync2(path) && statSync2(path).mtimeMs === modified && settled && readEverything) opts.onDrained?.();
671
- } catch {
1079
+ const progress = settled ? batch.acknowledge() : "unchanged";
1080
+ catchingUp = progress === "more";
1081
+ const health = captureHealth(state, stalled, catchingUp);
1082
+ await reportHealth(source === "cursor" && health === "capturing" ? "capturing_limited" : health);
1083
+ if (ending && opts.ending?.() && progress === "caught_up") {
1084
+ await reportHealth(health === "capture_failed" ? health : source === "cursor" ? "synced_limited" : "synced");
1085
+ opts.onDrained?.();
1086
+ }
1087
+ } catch (error) {
1088
+ await reportHealth(error instanceof TranscriptSourceUnavailable ? "source_unavailable" : "capture_failed");
672
1089
  } finally {
673
1090
  inFlight = false;
674
1091
  }
@@ -706,15 +1123,32 @@ var startTranscriptSync = (opts) => {
706
1123
 
707
1124
  // src/transcript-registration.ts
708
1125
  import { homedir as homedir2 } from "os";
709
- var defaultRegistryDirectory = () => process.env.PUSHARY_TRANSCRIPT_REGISTRY_DIR || join4(pusharyDir(), "transcript-capture");
1126
+ var storedRegistrationSchema = external_exports.object({
1127
+ sessionId: external_exports.string().min(1).max(128),
1128
+ cwd: external_exports.string(),
1129
+ path: external_exports.string(),
1130
+ source: external_exports.enum(["claude", "codex", "gemini", "cursor", "hermes"]),
1131
+ root: external_exports.string().optional(),
1132
+ account: external_exports.string().regex(/^[a-f0-9]{64}$/),
1133
+ consentOverride: external_exports.boolean().nullable().default(null),
1134
+ expectedSiteId: external_exports.string().min(1).optional(),
1135
+ expectedBaseUrl: external_exports.string().url().optional(),
1136
+ verifiedSiteId: external_exports.string().min(1).optional(),
1137
+ endedAt: external_exports.number().positive().optional(),
1138
+ drained: external_exports.boolean().optional()
1139
+ });
1140
+ var defaultRegistryDirectory = () => process.env.PUSHARY_TRANSCRIPT_REGISTRY_DIR || join5(pusharyDir(), "transcript-capture");
710
1141
  var cliRegistry = (apiKey = getApiKey()) => ({ apiKey, baseUrl: getBaseUrl(), directory: defaultRegistryDirectory() });
711
- var accountFor = (registry) => createHash("sha256").update(`${registry.baseUrl}\0${registry.apiKey}`).digest("hex");
1142
+ var accountFor = (registry) => createHash4("sha256").update(`${registry.baseUrl}\0${registry.apiKey}`).digest("hex");
712
1143
  var TTL = 48 * 60 * 60 * 1e3;
713
- var read = (path) => JSON.parse(readFileSync4(path, "utf8"));
714
- var consent = (record) => record.consentOverride ?? readTranscriptPreference() ?? false;
1144
+ var read = (path) => {
1145
+ if (statSync3(path).size > 16384) throw new Error("History registration exceeds its size limit");
1146
+ return storedRegistrationSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
1147
+ };
1148
+ var consent = (record) => record.consentOverride ?? readTranscriptPreference() ?? true;
715
1149
  var withoutTrailingSlash = (url) => url.replace(/\/+$/, "");
716
1150
  var sameServer = (url, registry) => withoutTrailingSlash(url) === withoutTrailingSlash(registry.baseUrl);
717
- var fileFor3 = (registry, sessionId) => join4(registry.directory, `${createHash("sha256").update(accountFor(registry) + sessionId).digest("hex")}.json`);
1151
+ var fileFor3 = (registry, sessionId) => join5(registry.directory, `${createHash4("sha256").update(accountFor(registry) + sessionId).digest("hex")}.json`);
718
1152
  var permitted = (registry, raw) => raw.account === accountFor(registry) && consent(raw) && !raw.drained && (raw.source !== "claude" || !!raw.expectedSiteId || !macOwnsClaudeHooks(raw.cwd)) && (!raw.expectedSiteId || raw.expectedSiteId === raw.verifiedSiteId && !!raw.expectedBaseUrl && sameServer(raw.expectedBaseUrl, registry));
719
1153
  var registeredTranscriptEnabled = (apiKey, sessionId, registry = cliRegistry(apiKey)) => {
720
1154
  try {
@@ -724,39 +1158,56 @@ var registeredTranscriptEnabled = (apiKey, sessionId, registry = cliRegistry(api
724
1158
  }
725
1159
  };
726
1160
  var macOwnsClaudeHooks = (cwd) => [
727
- join4(claudeConfigDir(), "settings.json"),
728
- join4(cwd, ".claude", "settings.json"),
729
- join4(cwd, ".claude", "settings.local.json")
1161
+ join5(claudeConfigDir(), "settings.json"),
1162
+ join5(cwd, ".claude", "settings.json"),
1163
+ join5(cwd, ".claude", "settings.local.json")
730
1164
  ].some((path) => {
731
1165
  try {
732
- return hookOwnerOf(JSON.parse(readFileSync4(path, "utf8"))) === "app";
1166
+ return hookOwnerOf(JSON.parse(readFileSync6(path, "utf8"))) === "app";
733
1167
  } catch {
734
1168
  return false;
735
1169
  }
736
1170
  });
737
- var TRANSCRIPT_ROOT_NAME = { claude: "projects", codex: "sessions" };
1171
+ var TRANSCRIPT_ROOT_NAME = { claude: "projects", codex: "sessions", gemini: "tmp", cursor: "projects", hermes: "hermes-history" };
738
1172
  var transcriptRoots = (source) => {
739
1173
  const home = homedir2();
740
- if (source === "codex") return [join4(codexHome(), TRANSCRIPT_ROOT_NAME.codex)];
1174
+ if (source === "codex") return [join5(codexHome(), TRANSCRIPT_ROOT_NAME.codex)];
1175
+ if (source === "cursor") return [join5(home, ".cursor", TRANSCRIPT_ROOT_NAME.cursor)];
1176
+ if (source === "hermes") return [join5(home, ".pushary", TRANSCRIPT_ROOT_NAME.hermes)];
1177
+ if (source === "gemini") return [join5(process.env.GEMINI_CLI_HOME || home, ".gemini", "tmp"), join5(home, ".cache", ".gemini", "tmp")];
741
1178
  const configured = process.env.CLAUDE_CONFIG_DIR;
742
- return [join4(configured || join4(home, ".claude"), TRANSCRIPT_ROOT_NAME.claude), join4(home, ".claude", TRANSCRIPT_ROOT_NAME.claude)];
1179
+ return [join5(configured || join5(home, ".claude"), TRANSCRIPT_ROOT_NAME.claude), join5(home, ".claude", TRANSCRIPT_ROOT_NAME.claude)];
743
1180
  };
744
1181
  var isAgentTranscriptRoot = (root, source) => isAbsolute(root) && resolve2(root) === root && basename(root) === TRANSCRIPT_ROOT_NAME[source];
745
1182
  var acceptedTranscriptRoots = (source, registeredRoot) => typeof registeredRoot === "string" && isAgentTranscriptRoot(registeredRoot, source) ? [...transcriptRoots(source), registeredRoot] : transcriptRoots(source);
1183
+ var resolvedSourcePath = (path) => {
1184
+ const absolute = resolve2(path);
1185
+ try {
1186
+ return realpathSync(absolute);
1187
+ } catch {
1188
+ const parent = dirname2(absolute);
1189
+ return parent === absolute ? absolute : join5(resolvedSourcePath(parent), basename(absolute));
1190
+ }
1191
+ };
746
1192
  var transcriptRootContaining = (candidate, roots) => {
747
- const target = resolve2(candidate);
748
- return roots.map((root) => resolve2(root)).find((root) => target.startsWith(root + sep)) ?? null;
1193
+ const target = resolvedSourcePath(candidate);
1194
+ return roots.map((root) => resolve2(root)).find((root) => target.startsWith(resolvedSourcePath(root) + sep)) ?? null;
749
1195
  };
750
1196
  var captureRegistration = (source, value, registeredRoot) => {
751
1197
  const input = value;
752
- if (source !== "claude" && source !== "codex") return null;
753
- if (input.agent_id || typeof input.session_id !== "string" || !/^[\w-]{1,128}$/.test(input.session_id)) return null;
1198
+ if (source !== "claude" && source !== "codex" && source !== "gemini" && source !== "cursor" && source !== "hermes") return null;
1199
+ const sessionId = input.session_id ?? input.conversation_id;
1200
+ if (input.agent_id || typeof sessionId !== "string" || !/^[\w-]{1,128}$/.test(sessionId)) return null;
754
1201
  if (typeof input.cwd !== "string" || !isAbsolute(input.cwd)) return null;
755
- const path = input.transcript_path ?? input.agent_transcript_path ?? (source === "claude" ? claudeTranscriptPath(input.cwd, input.session_id) : null);
756
- if (typeof path !== "string" || !isAbsolute(path) || !path.endsWith(".jsonl")) return null;
1202
+ const path = input.transcript_path ?? input.agent_transcript_path ?? (source === "claude" ? claudeTranscriptPath(input.cwd, sessionId) : null);
1203
+ if (typeof path !== "string" || !isAbsolute(path) || !(path.endsWith(".jsonl") || (source === "gemini" || source === "hermes") && path.endsWith(".json"))) return null;
757
1204
  const root = transcriptRootContaining(path, acceptedTranscriptRoots(source, registeredRoot));
758
1205
  if (root === null) return null;
759
- return { sessionId: input.session_id, cwd: input.cwd, path, source, root };
1206
+ const segments = relative(root, path).split(sep);
1207
+ if (source === "hermes" && (segments.length !== 2 || !/^[a-f0-9]{64}$/.test(segments[0] ?? "") || basename(path) !== `${sessionId}.json`)) return null;
1208
+ if (source === "gemini" && (segments.length !== 3 || segments[1] !== "chats" || !basename(path).startsWith("session-"))) return null;
1209
+ if (source === "cursor" && (segments[1] !== "agent-transcripts" || basename(path) !== `${sessionId}.jsonl` || !(segments.length === 3 || segments.length === 4 && basename(dirname2(path)) === sessionId))) return null;
1210
+ return { sessionId, cwd: input.cwd, path, source, root };
760
1211
  };
761
1212
  var registerTranscript = (source, input, startDaemon = () => {
762
1213
  void ensureDaemonRunning({ waitTimeoutMs: 0 }).catch(() => {
@@ -773,10 +1224,15 @@ var registerTranscript = (source, input, startDaemon = () => {
773
1224
  previous = read(path);
774
1225
  } catch {
775
1226
  }
1227
+ if (previous?.account === account && (resolvedSourcePath(previous.path) !== resolvedSourcePath(registration.path) || previous.source !== registration.source)) {
1228
+ process.stderr.write("[pushary] History registration refused a conflicting source for this session.\n");
1229
+ return;
1230
+ }
776
1231
  if (source === "claude" && !options.siteId && !previous?.expectedSiteId && macOwnsClaudeHooks(registration.cwd)) return;
777
1232
  const ending = options.ended || input.hook_event_name === "SessionEnd" && process.env.PUSHARY_WRAPPER_ACTIVE !== "1";
778
1233
  const stored = {
779
1234
  ...registration,
1235
+ path: previous?.account === account ? previous.path : registration.path,
780
1236
  account,
781
1237
  consentOverride: options.consentOverride ?? transcriptEnvironmentPreference(),
782
1238
  // A wrapper refresh must not erase the Mac's account binding.
@@ -803,7 +1259,8 @@ var sightRegisteredTranscripts = (epoch, registry, listing) => {
803
1259
  if (listing.complete) forgetUnregisteredTranscripts(epoch, registeredPaths);
804
1260
  };
805
1261
  var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
806
- const { intervalMs = 3e3, enabled = () => true, storage, epoch } = options;
1262
+ const { intervalMs = 3e3, enabled = () => true, storage } = options;
1263
+ const epoch = options.epoch ?? startTranscriptCaptureEpoch(Date.now(), void 0, SESSION_HISTORY_BACKFILL);
807
1264
  const registry = { apiKey, baseUrl: options.baseUrl ?? getBaseUrl(), directory: options.registryDirectory ?? defaultRegistryDirectory() };
808
1265
  const active = /* @__PURE__ */ new Map();
809
1266
  let stopped = false;
@@ -817,7 +1274,7 @@ var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
817
1274
  try {
818
1275
  let unreadable = 0;
819
1276
  const files = readdirSync2(registry.directory).filter((name) => name.endsWith(".json")).flatMap((name) => {
820
- const path = join4(registry.directory, name);
1277
+ const path = join5(registry.directory, name);
821
1278
  try {
822
1279
  const time = statSync3(path).mtimeMs;
823
1280
  if (Date.now() - time > TTL) {
@@ -866,13 +1323,18 @@ var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
866
1323
  for (const [id, registration] of wanted) {
867
1324
  if (active.has(id)) continue;
868
1325
  const current = () => read(registration.registrationPath);
1326
+ const snapshotKeys = join5(registry.directory, "snapshot-keys", accountFor(registry));
1327
+ const keyStorage = storage ?? (registration.source === "claude" || registration.source === "codex" ? void 0 : {
1328
+ sessionKeyDirectory: snapshotKeys,
1329
+ recipientPinFile: join5(snapshotKeys, "recipients.json")
1330
+ });
869
1331
  active.set(id, { path: registration.path, stop: startTranscriptSync({
870
1332
  apiKey,
871
1333
  machineId,
872
1334
  ...registration,
873
1335
  intervalMs,
874
1336
  baseUrl: registry.baseUrl,
875
- storage,
1337
+ storage: keyStorage,
876
1338
  epoch,
877
1339
  enabled: () => {
878
1340
  try {