sessionpipe 0.1.0 → 0.1.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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe 0.1.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.1.1 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/http-sink.ts
4
4
  import { createHmac, randomUUID } from "node:crypto";
@@ -86,12 +86,31 @@ var HttpSink = class {
86
86
  };
87
87
 
88
88
  // src/run.ts
89
- import { appendFileSync as appendFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
89
+ import {
90
+ appendFileSync as appendFileSync3,
91
+ existsSync as existsSync8,
92
+ mkdirSync as mkdirSync7,
93
+ readdirSync as readdirSync6,
94
+ readFileSync as readFileSync8,
95
+ renameSync as renameSync2,
96
+ statSync as statSync5,
97
+ unlinkSync as unlinkSync4,
98
+ writeFileSync as writeFileSync6
99
+ } from "node:fs";
90
100
  import os2 from "node:os";
91
101
  import path11 from "node:path";
92
102
 
93
103
  // ../core/src/adapters/antigravity.ts
94
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
104
+ import {
105
+ existsSync as existsSync2,
106
+ mkdirSync as mkdirSync2,
107
+ readdirSync as readdirSync2,
108
+ readFileSync as readFileSync2,
109
+ realpathSync,
110
+ statSync as statSync2,
111
+ unlinkSync as unlinkSync2,
112
+ writeFileSync as writeFileSync2
113
+ } from "node:fs";
95
114
  import path4 from "node:path";
96
115
 
97
116
  // ../core/src/paths.ts
@@ -318,16 +337,42 @@ function readAntigravity(lines, from) {
318
337
  }
319
338
 
320
339
  // ../core/src/adapters/claude-shaped.ts
321
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
340
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
322
341
  import path3 from "node:path";
323
342
  var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
324
343
  var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
325
- function readJson(file) {
344
+ function readJsonFile(file) {
345
+ let raw;
326
346
  try {
327
- return JSON.parse(readFileSync(file, "utf8"));
347
+ raw = readFileSync(file, "utf8");
328
348
  } catch {
329
- return {};
349
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
330
350
  }
351
+ let value;
352
+ try {
353
+ value = JSON.parse(raw);
354
+ } catch {
355
+ const hint = /^\s*\/\/|\/\*/m.test(raw) ? "not plain JSON (it has comments)" : "not plain JSON";
356
+ return { ok: false, existed: true, reason: hint };
357
+ }
358
+ if (!value || typeof value !== "object" || Array.isArray(value))
359
+ return { ok: false, existed: true, reason: "not a JSON object" };
360
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
361
+ const m = /^( +|\t+)\S/m.exec(raw);
362
+ const indent = m?.[1] ?? " ";
363
+ return {
364
+ ok: true,
365
+ existed: true,
366
+ value,
367
+ raw,
368
+ indent,
369
+ eol,
370
+ finalNewline: /\r?\n$/.test(raw)
371
+ };
372
+ }
373
+ function formatJson(value, f) {
374
+ const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
375
+ return f.finalNewline ? text + f.eol : text;
331
376
  }
332
377
  function entryFor(event, harness, cmd, o) {
333
378
  const matcher = o.matcherFor(event);
@@ -337,9 +382,22 @@ function entryFor(event, harness, cmd, o) {
337
382
  };
338
383
  }
339
384
  function installClaudeShaped(file, harness, events, cmd, o) {
340
- const settings2 = readJson(file);
385
+ const f = readJsonFile(file);
386
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
387
+ const settings2 = f.value;
341
388
  const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
342
389
  let changed = false;
390
+ const want = new Set(events);
391
+ for (const ev of Object.keys(hooks)) {
392
+ if (want.has(ev)) continue;
393
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
394
+ const kept = list.filter((h) => !isOurs(h));
395
+ if (kept.length !== list.length) {
396
+ changed = true;
397
+ if (kept.length) hooks[ev] = kept;
398
+ else delete hooks[ev];
399
+ }
400
+ }
343
401
  for (const ev of events) {
344
402
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
345
403
  const entry2 = entryFor(ev, harness, cmd, o);
@@ -358,19 +416,19 @@ function installClaudeShaped(file, harness, events, cmd, o) {
358
416
  if (changed) {
359
417
  settings2.hooks = hooks;
360
418
  mkdirSync(path3.dirname(file), { recursive: true });
361
- writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
362
- `);
419
+ writeFileSync(file, formatJson(settings2, f));
363
420
  }
364
- return { file, changed };
421
+ return { file, changed, created: !f.existed && changed };
365
422
  }
366
- function uninstallClaudeShaped(file, events) {
423
+ function uninstallClaudeShaped(file, events, opts = {}) {
367
424
  if (!existsSync(file)) return { file, changed: false };
368
- const raw = readFileSync(file, "utf8");
369
- const settings2 = readJson(file);
425
+ const f = readJsonFile(file);
426
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
427
+ const settings2 = f.value;
370
428
  const hooks = settings2.hooks;
371
429
  if (!hooks || typeof hooks !== "object") return { file, changed: false };
372
430
  let changed = false;
373
- for (const ev of events) {
431
+ for (const ev of [...events, ...Object.keys(hooks)]) {
374
432
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
375
433
  const kept = list.filter((h) => !isOurs(h));
376
434
  if (kept.length !== list.length) {
@@ -381,13 +439,17 @@ function uninstallClaudeShaped(file, events) {
381
439
  }
382
440
  if (!changed) return { file, changed: false };
383
441
  if (!Object.keys(hooks).length) delete settings2.hooks;
384
- const next = `${JSON.stringify(settings2, null, 2)}
385
- `;
386
- if (next !== raw) writeFileSync(file, next);
442
+ if (opts.created && !Object.keys(settings2).length) {
443
+ unlinkSync(file);
444
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
445
+ }
446
+ const next = formatJson(settings2, f);
447
+ if (next !== f.raw) writeFileSync(file, next);
387
448
  return { file, changed: true };
388
449
  }
389
450
  function installedClaudeShaped(file, harness, events, cmd, o) {
390
- const hooks = readJson(file).hooks ?? {};
451
+ const f = readJsonFile(file);
452
+ const hooks = f.ok ? f.value.hooks ?? {} : {};
391
453
  let present = 0;
392
454
  let current = 0;
393
455
  for (const ev of events) {
@@ -396,6 +458,9 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
396
458
  if (mine.length) present++;
397
459
  if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
398
460
  }
461
+ for (const ev of Object.keys(hooks))
462
+ if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
463
+ return "stale";
399
464
  if (!present) return "missing";
400
465
  return current === events.length ? "current" : "stale";
401
466
  }
@@ -410,10 +475,13 @@ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
410
475
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
411
476
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
412
477
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
413
- function entry(cmd) {
478
+ function entry(cmd, lean) {
414
479
  const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
415
480
  const out = { enabled: true };
416
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
481
+ for (const e of AG_EVENTS) {
482
+ if (lean && e === "PreToolUse") continue;
483
+ out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
484
+ }
417
485
  return out;
418
486
  }
419
487
  var antigravity = {
@@ -421,34 +489,49 @@ var antigravity = {
421
489
  events: AG_EVENTS,
422
490
  detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
423
491
  configFiles: (env = process.env) => [hooksFile(env)],
424
- install(cmd, env = process.env) {
492
+ install(cmd, env = process.env, opts = {}) {
425
493
  const file = hooksFile(env);
426
- const hooks = readJson(file);
427
- const e = entry(cmd);
494
+ const f = readJsonFile(file);
495
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
496
+ const hooks = f.value;
497
+ const e = entry(cmd, opts.lean);
428
498
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
429
499
  hooks[HOOK_NAME] = e;
430
500
  mkdirSync2(path4.dirname(file), { recursive: true });
431
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
432
- `);
501
+ writeFileSync2(file, formatJson(hooks, f));
433
502
  return [
434
- { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
503
+ {
504
+ file,
505
+ changed: true,
506
+ created: !f.existed,
507
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
508
+ }
435
509
  ];
436
510
  },
437
- uninstall(env = process.env) {
511
+ uninstall(env = process.env, opts = {}) {
438
512
  const file = hooksFile(env);
439
513
  if (!existsSync2(file)) return [{ file, changed: false }];
440
- const hooks = readJson(file);
514
+ const f = readJsonFile(file);
515
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
516
+ const hooks = f.value;
441
517
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
442
518
  delete hooks[HOOK_NAME];
443
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
444
- `);
519
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
520
+ unlinkSync2(file);
521
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
522
+ }
523
+ writeFileSync2(file, formatJson(hooks, f));
445
524
  return [{ file, changed: true }];
446
525
  },
447
- installed(cmd, env = process.env) {
526
+ installed(cmd, env = process.env, opts = {}) {
448
527
  const file = hooksFile(env);
449
- const have = readJson(file)[HOOK_NAME];
528
+ const f = readJsonFile(file);
529
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
450
530
  return [
451
- { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale" }
531
+ {
532
+ file,
533
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd, opts.lean)) ? "current" : "stale"
534
+ }
452
535
  ];
453
536
  },
454
537
  fromHook(input) {
@@ -714,8 +797,11 @@ var CLAUDE_EVENTS = [
714
797
  "SubagentStop",
715
798
  "PreCompact",
716
799
  "PostCompact",
717
- "SessionEnd"
800
+ "SessionEnd",
801
+ "StopFailure"
718
802
  ];
803
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
804
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
719
805
  var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
720
806
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
721
807
  function claudeDirs(env = process.env, home = HOME) {
@@ -747,11 +833,13 @@ var claudeCode = {
747
833
  events: CLAUDE_EVENTS,
748
834
  detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
749
835
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
750
- install: (cmd, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)),
751
- uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
752
- installed: (cmd, env = process.env) => claudeDirs(env).map((d) => ({
836
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
837
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
838
+ (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
839
+ ),
840
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
753
841
  file: settingsIn(d),
754
- state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)
842
+ state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
755
843
  })),
756
844
  fromHook(input) {
757
845
  const event = input.argv[1] ?? "";
@@ -842,6 +930,15 @@ var claudeCode = {
842
930
  case "Stop":
843
931
  events.push(ev("turn.ended", { turn_id, reason: "stop" }));
844
932
  break;
933
+ case "StopFailure":
934
+ events.push(
935
+ ev("turn.ended", {
936
+ turn_id,
937
+ reason: "error",
938
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
939
+ })
940
+ );
941
+ break;
845
942
  case "SubagentStart":
846
943
  events.push(
847
944
  ev("subagent.started", {
@@ -1353,6 +1450,7 @@ var CODEX_EVENTS = [
1353
1450
  "SessionEnd"
1354
1451
  ];
1355
1452
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1453
+ var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1356
1454
  var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1357
1455
  var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1358
1456
  var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
@@ -1401,7 +1499,8 @@ function installNotify(cmd, env) {
1401
1499
  return {
1402
1500
  file,
1403
1501
  changed: false,
1404
- note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1502
+ skipped: true,
1503
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. Codex will report nothing until you approve the hooks: start \`codex\` and accept the hook review`
1405
1504
  };
1406
1505
  }
1407
1506
  write(withNotify(text, line));
@@ -1458,14 +1557,21 @@ var codex = {
1458
1557
  events: CODEX_EVENTS,
1459
1558
  detect: (env = process.env) => existsSync4(codexHome(env)),
1460
1559
  configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1461
- install: (cmd, env = process.env) => [
1462
- installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2),
1560
+ install: (cmd, env = process.env, opts = {}) => [
1561
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1463
1562
  installNotify(cmd, env)
1464
1563
  ],
1465
- uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1466
- installed: (cmd, env = process.env) => {
1564
+ uninstall: (env = process.env, opts = {}) => [
1565
+ uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1566
+ uninstallNotify(env)
1567
+ ],
1568
+ installed: (cmd, env = process.env, opts = {}) => {
1467
1569
  const out = [
1468
- { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2) }
1570
+ {
1571
+ file: hooksFile2(env),
1572
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1573
+ note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1574
+ }
1469
1575
  ];
1470
1576
  let text = "";
1471
1577
  try {
@@ -1653,6 +1759,7 @@ function codexFacts(file) {
1653
1759
  }
1654
1760
 
1655
1761
  // ../core/src/adapters/gemini-cli.ts
1762
+ import { execFileSync as execFileSync3 } from "node:child_process";
1656
1763
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1657
1764
  import path7 from "node:path";
1658
1765
  var NAME4 = "gemini-cli";
@@ -1670,17 +1777,61 @@ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1670
1777
  var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1671
1778
  var settings = (env) => path7.join(geminiDir(env), "settings.json");
1672
1779
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1780
+ var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1781
+ function geminiHooksActive(settingsFile) {
1782
+ let tools;
1783
+ try {
1784
+ tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1785
+ } catch {
1786
+ }
1787
+ if (tools?.enableHooks === true) return { active: true };
1788
+ if (tools?.enableHooks === false)
1789
+ return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1790
+ let version = "";
1791
+ try {
1792
+ version = execFileSync3("gemini", ["--version"], {
1793
+ encoding: "utf8",
1794
+ timeout: 4e3,
1795
+ stdio: ["ignore", "pipe", "ignore"]
1796
+ }).trim();
1797
+ } catch {
1798
+ return {
1799
+ active: true,
1800
+ note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
1801
+ };
1802
+ }
1803
+ const m = /(\d+)\.(\d+)/.exec(version);
1804
+ if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1805
+ return {
1806
+ active: false,
1807
+ note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1808
+ };
1809
+ }
1673
1810
  var geminiCli = {
1674
1811
  name: NAME4,
1675
1812
  events: GEMINI_EVENTS,
1676
1813
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1677
1814
  detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1678
1815
  configFiles: (env = process.env) => [settings(env)],
1679
- install: (cmd, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3)],
1680
- uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1681
- installed: (cmd, env = process.env) => [
1682
- { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3) }
1816
+ install: (cmd, env = process.env, opts = {}) => {
1817
+ const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1818
+ const a = geminiHooksActive(settings(env));
1819
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1820
+ },
1821
+ uninstall: (env = process.env, opts = {}) => [
1822
+ uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1683
1823
  ],
1824
+ installed: (cmd, env = process.env, opts = {}) => {
1825
+ const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1826
+ const a = geminiHooksActive(settings(env));
1827
+ return [
1828
+ {
1829
+ file: settings(env),
1830
+ state: state === "current" && !a.active ? "inactive" : state,
1831
+ ...a.note ? { note: a.note } : {}
1832
+ }
1833
+ ];
1834
+ },
1684
1835
  fromHook(input) {
1685
1836
  const event = input.argv[1] ?? "";
1686
1837
  let s = {};
@@ -1891,7 +2042,7 @@ import {
1891
2042
  readSync as readSync2,
1892
2043
  renameSync,
1893
2044
  statSync as statSync4,
1894
- unlinkSync,
2045
+ unlinkSync as unlinkSync3,
1895
2046
  writeFileSync as writeFileSync5
1896
2047
  } from "node:fs";
1897
2048
  import path9 from "node:path";
@@ -1919,8 +2070,8 @@ var Outbox = class {
1919
2070
  } catch {
1920
2071
  n = this.lastSeqInFile(ref) + 1;
1921
2072
  }
1922
- mkdirSync5(path9.dirname(f), { recursive: true });
1923
- writeFileSync5(f, String(n + 1));
2073
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
2074
+ writeFileSync5(f, String(n + 1), { mode: 384 });
1924
2075
  return n;
1925
2076
  }
1926
2077
  lastSeqInFile(ref) {
@@ -1938,9 +2089,9 @@ var Outbox = class {
1938
2089
  }
1939
2090
  append(ref, events) {
1940
2091
  if (!events.length) return;
1941
- mkdirSync5(this.dir(ref.harness), { recursive: true });
2092
+ mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
1942
2093
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
1943
- `);
2094
+ `, { mode: 384 });
1944
2095
  }
1945
2096
  /** Every session file, newest first. */
1946
2097
  sessions() {
@@ -2004,9 +2155,9 @@ var Outbox = class {
2004
2155
  const f = this.file(ref);
2005
2156
  try {
2006
2157
  if (statSync4(f).mtimeMs < cutoff) {
2007
- unlinkSync(f);
2158
+ unlinkSync3(f);
2008
2159
  try {
2009
- unlinkSync(this.seqFile(ref));
2160
+ unlinkSync3(this.seqFile(ref));
2010
2161
  } catch {
2011
2162
  }
2012
2163
  n++;
@@ -2019,7 +2170,7 @@ var Outbox = class {
2019
2170
  delete(ref) {
2020
2171
  for (const f of [this.file(ref), this.seqFile(ref)]) {
2021
2172
  try {
2022
- unlinkSync(f);
2173
+ unlinkSync3(f);
2023
2174
  } catch {
2024
2175
  }
2025
2176
  }
@@ -2044,9 +2195,9 @@ var Cursors = class {
2044
2195
  }
2045
2196
  write(c) {
2046
2197
  const f = this.file();
2047
- mkdirSync5(path9.dirname(f), { recursive: true });
2198
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
2048
2199
  const tmp = `${f}.${process.pid}.tmp`;
2049
- writeFileSync5(tmp, JSON.stringify(c));
2200
+ writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2050
2201
  renameSync(tmp, f);
2051
2202
  }
2052
2203
  key(ref) {
@@ -2055,10 +2206,10 @@ var Cursors = class {
2055
2206
  };
2056
2207
  function takeLock(root, ref, staleMs = 5 * 6e4) {
2057
2208
  const dir = path9.join(root, "locks");
2058
- mkdirSync5(dir, { recursive: true });
2209
+ mkdirSync5(dir, { recursive: true, mode: 448 });
2059
2210
  const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
2060
2211
  try {
2061
- if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync(f);
2212
+ if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync3(f);
2062
2213
  else return null;
2063
2214
  } catch {
2064
2215
  }
@@ -2069,7 +2220,7 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2069
2220
  }
2070
2221
  return () => {
2071
2222
  try {
2072
- unlinkSync(f);
2223
+ unlinkSync3(f);
2073
2224
  } catch {
2074
2225
  }
2075
2226
  };
@@ -2205,7 +2356,7 @@ function redactAssignments(text) {
2205
2356
  function isSecretName(name) {
2206
2357
  if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
2207
2358
  const flat = name.toLowerCase().replace(/[_-]/g, "");
2208
- return /(?:pass(?:word|wd|phrase|code)?|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2359
+ return /(?:pass(?:word|wd|phrase|code)|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2209
2360
  flat
2210
2361
  );
2211
2362
  }
@@ -2294,7 +2445,7 @@ var DATA_ALLOW = {
2294
2445
  "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2295
2446
  "session.forgotten": { 0: [] },
2296
2447
  "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2297
- "turn.ended": { 0: ["turn_id", "reason", "ms"] },
2448
+ "turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
2298
2449
  "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2299
2450
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
2300
2451
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
@@ -2416,9 +2567,16 @@ var StdoutSink = class {
2416
2567
  };
2417
2568
 
2418
2569
  // src/run.ts
2570
+ function enqueueJob(state, job) {
2571
+ const jobs = jobsDir(state);
2572
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
2573
+ const f = path11.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
2574
+ writeFileSync6(f, JSON.stringify(job), { mode: 384 });
2575
+ return f;
2576
+ }
2419
2577
  function log(state, line) {
2420
2578
  try {
2421
- mkdirSync7(state, { recursive: true });
2579
+ mkdirSync7(state, { recursive: true, mode: 448 });
2422
2580
  const f = path11.join(state, "log");
2423
2581
  try {
2424
2582
  if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
@@ -2443,8 +2601,8 @@ function factsState(state) {
2443
2601
  const cur = this.get(h, s);
2444
2602
  const next = { ...cur, ...patch };
2445
2603
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
2446
- mkdirSync7(path11.dirname(file(h, s)), { recursive: true });
2447
- writeFileSync6(file(h, s), JSON.stringify(next));
2604
+ mkdirSync7(path11.dirname(file(h, s)), { recursive: true, mode: 448 });
2605
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
2448
2606
  }
2449
2607
  };
2450
2608
  }
@@ -2469,12 +2627,14 @@ async function runJob(job, opts = {}) {
2469
2627
  if (!r || !r.session.id) return { events: [], delivered: {} };
2470
2628
  const outbox = new Outbox(state);
2471
2629
  const ref = { harness: job.harness, session: r.session.id };
2472
- const release = takeLock(state, ref);
2630
+ let release = takeLock(state, ref);
2631
+ for (let wait = 50; !release && wait <= 3200; wait *= 2) {
2632
+ await new Promise((res) => setTimeout(res, wait));
2633
+ release = takeLock(state, ref);
2634
+ }
2473
2635
  if (!release) {
2474
- log(state, `${job.harness} ${job.event}: session ${r.session.id} locked, re-queued`);
2475
- const jobs = path11.join(state, "jobs");
2476
- mkdirSync7(jobs, { recursive: true });
2477
- writeFileSync6(path11.join(jobs, `${Date.now().toString(36)}-requeue.json`), JSON.stringify(job));
2636
+ log(state, `${job.harness} ${job.event}: session ${r.session.id} locked for 6 s, re-queued`);
2637
+ enqueueJob(state, job);
2478
2638
  return { events: [], delivered: {} };
2479
2639
  }
2480
2640
  try {
@@ -2538,7 +2698,10 @@ async function runJob(job, opts = {}) {
2538
2698
  if (read.turns.length)
2539
2699
  facts.save(job.harness, r.session.id, { transcript_line: read.line, transcript_turns: turnNo });
2540
2700
  }
2541
- outbox.append(ref, events);
2701
+ outbox.append(
2702
+ ref,
2703
+ events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
2704
+ );
2542
2705
  log(
2543
2706
  state,
2544
2707
  `${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${r.session.id.slice(0, 8)})`
@@ -2564,34 +2727,57 @@ async function flush(state, sinkConfigs, outbox, first) {
2564
2727
  const cursors = new Cursors(state, sink.name);
2565
2728
  const c = cursors.read();
2566
2729
  let sent = 0;
2730
+ let stalled = false;
2731
+ let batch = [];
2732
+ let pending = {};
2733
+ const send = async () => {
2734
+ if (!batch.length) {
2735
+ Object.assign(c, pending);
2736
+ pending = {};
2737
+ cursors.write(c);
2738
+ return true;
2739
+ }
2740
+ const res = await sink.send(batch);
2741
+ if (!res.ok) {
2742
+ log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2743
+ if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2744
+ if (res.action !== "drop") {
2745
+ batch = [];
2746
+ pending = {};
2747
+ return false;
2748
+ }
2749
+ }
2750
+ sent += batch.length;
2751
+ Object.assign(c, pending);
2752
+ cursors.write(c);
2753
+ batch = [];
2754
+ pending = {};
2755
+ return true;
2756
+ };
2567
2757
  for (const ref of refs) {
2568
- if (sent >= MAX_PER_RUN) break;
2569
- let offset = c[cursors.key(ref)] ?? 0;
2570
- let stalled = false;
2571
- while (sent < MAX_PER_RUN) {
2572
- const { events, offset: next } = outbox.read(ref, offset, BATCH);
2758
+ if (stalled || sent >= MAX_PER_RUN) break;
2759
+ let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
2760
+ for (; ; ) {
2761
+ const room = BATCH - batch.length;
2762
+ const { events, offset: next } = outbox.read(ref, offset, room);
2573
2763
  if (!events.length) break;
2574
- const filtered = events.map((e) => filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() })).filter((e) => !!e);
2575
- let ok = true;
2576
- if (filtered.length) {
2577
- const res = await sink.send(filtered);
2578
- ok = res.ok;
2579
- if (!ok) {
2580
- log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2581
- if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2582
- if (res.action === "drop") ok = true;
2583
- }
2584
- if (ok) sent += filtered.length;
2585
- }
2586
- if (!ok) {
2587
- stalled = true;
2588
- break;
2764
+ for (const e of events) {
2765
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() });
2766
+ if (f) batch.push(f);
2589
2767
  }
2590
2768
  offset = next;
2591
- c[cursors.key(ref)] = offset;
2592
- cursors.write(c);
2769
+ pending[cursors.key(ref)] = offset;
2770
+ if (batch.length >= BATCH) {
2771
+ if (!await send()) {
2772
+ stalled = true;
2773
+ break;
2774
+ }
2775
+ if (sent >= MAX_PER_RUN) break;
2776
+ }
2593
2777
  }
2594
- if (stalled) break;
2778
+ }
2779
+ if (!stalled && (batch.length || Object.keys(pending).length)) {
2780
+ if (!await send()) stalled = true;
2595
2781
  }
2596
2782
  delivered[sink.name] = sent;
2597
2783
  }
@@ -2608,6 +2794,7 @@ function applySinkVerdict(name, res) {
2608
2794
  } catch {
2609
2795
  }
2610
2796
  }
2797
+ var jobsDir = (state) => path11.join(state, "jobs");
2611
2798
  export {
2612
2799
  HttpSink,
2613
2800
  buildSinks,