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/worker.js CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.1.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.1.1 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/worker.ts
5
- import { readFileSync as readFileSync9, unlinkSync as unlinkSync2 } from "node:fs";
6
-
7
- // src/run.ts
8
- 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";
9
- import os2 from "node:os";
10
- import path11 from "node:path";
5
+ import { readFileSync as readFileSync9, unlinkSync as unlinkSync5 } from "node:fs";
11
6
 
12
7
  // ../core/src/adapters/antigravity.ts
13
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
8
+ import {
9
+ existsSync as existsSync2,
10
+ mkdirSync as mkdirSync2,
11
+ readdirSync as readdirSync2,
12
+ readFileSync as readFileSync2,
13
+ realpathSync,
14
+ statSync as statSync2,
15
+ unlinkSync as unlinkSync2,
16
+ writeFileSync as writeFileSync2
17
+ } from "node:fs";
14
18
  import path4 from "node:path";
15
19
 
16
20
  // ../core/src/paths.ts
@@ -237,16 +241,42 @@ function readAntigravity(lines, from) {
237
241
  }
238
242
 
239
243
  // ../core/src/adapters/claude-shaped.ts
240
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
244
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
241
245
  import path3 from "node:path";
242
246
  var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
243
247
  var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
244
- function readJson(file) {
248
+ function readJsonFile(file) {
249
+ let raw;
245
250
  try {
246
- return JSON.parse(readFileSync(file, "utf8"));
251
+ raw = readFileSync(file, "utf8");
247
252
  } catch {
248
- return {};
253
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
249
254
  }
255
+ let value;
256
+ try {
257
+ value = JSON.parse(raw);
258
+ } catch {
259
+ const hint = /^\s*\/\/|\/\*/m.test(raw) ? "not plain JSON (it has comments)" : "not plain JSON";
260
+ return { ok: false, existed: true, reason: hint };
261
+ }
262
+ if (!value || typeof value !== "object" || Array.isArray(value))
263
+ return { ok: false, existed: true, reason: "not a JSON object" };
264
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
265
+ const m = /^( +|\t+)\S/m.exec(raw);
266
+ const indent = m?.[1] ?? " ";
267
+ return {
268
+ ok: true,
269
+ existed: true,
270
+ value,
271
+ raw,
272
+ indent,
273
+ eol,
274
+ finalNewline: /\r?\n$/.test(raw)
275
+ };
276
+ }
277
+ function formatJson(value, f) {
278
+ const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
279
+ return f.finalNewline ? text + f.eol : text;
250
280
  }
251
281
  function entryFor(event, harness, cmd, o) {
252
282
  const matcher = o.matcherFor(event);
@@ -256,9 +286,22 @@ function entryFor(event, harness, cmd, o) {
256
286
  };
257
287
  }
258
288
  function installClaudeShaped(file, harness, events, cmd, o) {
259
- const settings2 = readJson(file);
289
+ const f = readJsonFile(file);
290
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
291
+ const settings2 = f.value;
260
292
  const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
261
293
  let changed = false;
294
+ const want = new Set(events);
295
+ for (const ev of Object.keys(hooks)) {
296
+ if (want.has(ev)) continue;
297
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
298
+ const kept = list.filter((h) => !isOurs(h));
299
+ if (kept.length !== list.length) {
300
+ changed = true;
301
+ if (kept.length) hooks[ev] = kept;
302
+ else delete hooks[ev];
303
+ }
304
+ }
262
305
  for (const ev of events) {
263
306
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
264
307
  const entry2 = entryFor(ev, harness, cmd, o);
@@ -277,19 +320,19 @@ function installClaudeShaped(file, harness, events, cmd, o) {
277
320
  if (changed) {
278
321
  settings2.hooks = hooks;
279
322
  mkdirSync(path3.dirname(file), { recursive: true });
280
- writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
281
- `);
323
+ writeFileSync(file, formatJson(settings2, f));
282
324
  }
283
- return { file, changed };
325
+ return { file, changed, created: !f.existed && changed };
284
326
  }
285
- function uninstallClaudeShaped(file, events) {
327
+ function uninstallClaudeShaped(file, events, opts = {}) {
286
328
  if (!existsSync(file)) return { file, changed: false };
287
- const raw = readFileSync(file, "utf8");
288
- const settings2 = readJson(file);
329
+ const f = readJsonFile(file);
330
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
331
+ const settings2 = f.value;
289
332
  const hooks = settings2.hooks;
290
333
  if (!hooks || typeof hooks !== "object") return { file, changed: false };
291
334
  let changed = false;
292
- for (const ev of events) {
335
+ for (const ev of [...events, ...Object.keys(hooks)]) {
293
336
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
294
337
  const kept = list.filter((h) => !isOurs(h));
295
338
  if (kept.length !== list.length) {
@@ -300,13 +343,17 @@ function uninstallClaudeShaped(file, events) {
300
343
  }
301
344
  if (!changed) return { file, changed: false };
302
345
  if (!Object.keys(hooks).length) delete settings2.hooks;
303
- const next = `${JSON.stringify(settings2, null, 2)}
304
- `;
305
- if (next !== raw) writeFileSync(file, next);
346
+ if (opts.created && !Object.keys(settings2).length) {
347
+ unlinkSync(file);
348
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
349
+ }
350
+ const next = formatJson(settings2, f);
351
+ if (next !== f.raw) writeFileSync(file, next);
306
352
  return { file, changed: true };
307
353
  }
308
354
  function installedClaudeShaped(file, harness, events, cmd, o) {
309
- const hooks = readJson(file).hooks ?? {};
355
+ const f = readJsonFile(file);
356
+ const hooks = f.ok ? f.value.hooks ?? {} : {};
310
357
  let present = 0;
311
358
  let current = 0;
312
359
  for (const ev of events) {
@@ -315,6 +362,9 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
315
362
  if (mine.length) present++;
316
363
  if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
317
364
  }
365
+ for (const ev of Object.keys(hooks))
366
+ if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
367
+ return "stale";
318
368
  if (!present) return "missing";
319
369
  return current === events.length ? "current" : "stale";
320
370
  }
@@ -329,10 +379,13 @@ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
329
379
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
330
380
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
331
381
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
332
- function entry(cmd) {
382
+ function entry(cmd, lean) {
333
383
  const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
334
384
  const out = { enabled: true };
335
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
385
+ for (const e of AG_EVENTS) {
386
+ if (lean && e === "PreToolUse") continue;
387
+ out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
388
+ }
336
389
  return out;
337
390
  }
338
391
  var antigravity = {
@@ -340,34 +393,49 @@ var antigravity = {
340
393
  events: AG_EVENTS,
341
394
  detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
342
395
  configFiles: (env = process.env) => [hooksFile(env)],
343
- install(cmd, env = process.env) {
396
+ install(cmd, env = process.env, opts = {}) {
344
397
  const file = hooksFile(env);
345
- const hooks = readJson(file);
346
- const e = entry(cmd);
398
+ const f = readJsonFile(file);
399
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
400
+ const hooks = f.value;
401
+ const e = entry(cmd, opts.lean);
347
402
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
348
403
  hooks[HOOK_NAME] = e;
349
404
  mkdirSync2(path4.dirname(file), { recursive: true });
350
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
351
- `);
405
+ writeFileSync2(file, formatJson(hooks, f));
352
406
  return [
353
- { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
407
+ {
408
+ file,
409
+ changed: true,
410
+ created: !f.existed,
411
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
412
+ }
354
413
  ];
355
414
  },
356
- uninstall(env = process.env) {
415
+ uninstall(env = process.env, opts = {}) {
357
416
  const file = hooksFile(env);
358
417
  if (!existsSync2(file)) return [{ file, changed: false }];
359
- const hooks = readJson(file);
418
+ const f = readJsonFile(file);
419
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
420
+ const hooks = f.value;
360
421
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
361
422
  delete hooks[HOOK_NAME];
362
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
363
- `);
423
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
424
+ unlinkSync2(file);
425
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
426
+ }
427
+ writeFileSync2(file, formatJson(hooks, f));
364
428
  return [{ file, changed: true }];
365
429
  },
366
- installed(cmd, env = process.env) {
430
+ installed(cmd, env = process.env, opts = {}) {
367
431
  const file = hooksFile(env);
368
- const have = readJson(file)[HOOK_NAME];
432
+ const f = readJsonFile(file);
433
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
369
434
  return [
370
- { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale" }
435
+ {
436
+ file,
437
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd, opts.lean)) ? "current" : "stale"
438
+ }
371
439
  ];
372
440
  },
373
441
  fromHook(input) {
@@ -633,8 +701,11 @@ var CLAUDE_EVENTS = [
633
701
  "SubagentStop",
634
702
  "PreCompact",
635
703
  "PostCompact",
636
- "SessionEnd"
704
+ "SessionEnd",
705
+ "StopFailure"
637
706
  ];
707
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
708
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
638
709
  var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
639
710
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
640
711
  function claudeDirs(env = process.env, home = HOME) {
@@ -666,11 +737,13 @@ var claudeCode = {
666
737
  events: CLAUDE_EVENTS,
667
738
  detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
668
739
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
669
- install: (cmd, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)),
670
- uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
671
- installed: (cmd, env = process.env) => claudeDirs(env).map((d) => ({
740
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
741
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
742
+ (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
743
+ ),
744
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
672
745
  file: settingsIn(d),
673
- state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)
746
+ state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
674
747
  })),
675
748
  fromHook(input) {
676
749
  const event = input.argv[1] ?? "";
@@ -761,6 +834,15 @@ var claudeCode = {
761
834
  case "Stop":
762
835
  events.push(ev("turn.ended", { turn_id, reason: "stop" }));
763
836
  break;
837
+ case "StopFailure":
838
+ events.push(
839
+ ev("turn.ended", {
840
+ turn_id,
841
+ reason: "error",
842
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
843
+ })
844
+ );
845
+ break;
764
846
  case "SubagentStart":
765
847
  events.push(
766
848
  ev("subagent.started", {
@@ -1272,6 +1354,7 @@ var CODEX_EVENTS = [
1272
1354
  "SessionEnd"
1273
1355
  ];
1274
1356
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1357
+ var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1275
1358
  var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1276
1359
  var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1277
1360
  var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
@@ -1320,7 +1403,8 @@ function installNotify(cmd, env) {
1320
1403
  return {
1321
1404
  file,
1322
1405
  changed: false,
1323
- note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1406
+ skipped: true,
1407
+ 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`
1324
1408
  };
1325
1409
  }
1326
1410
  write(withNotify(text, line));
@@ -1377,14 +1461,21 @@ var codex = {
1377
1461
  events: CODEX_EVENTS,
1378
1462
  detect: (env = process.env) => existsSync4(codexHome(env)),
1379
1463
  configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1380
- install: (cmd, env = process.env) => [
1381
- installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2),
1464
+ install: (cmd, env = process.env, opts = {}) => [
1465
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1382
1466
  installNotify(cmd, env)
1383
1467
  ],
1384
- uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1385
- installed: (cmd, env = process.env) => {
1468
+ uninstall: (env = process.env, opts = {}) => [
1469
+ uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1470
+ uninstallNotify(env)
1471
+ ],
1472
+ installed: (cmd, env = process.env, opts = {}) => {
1386
1473
  const out = [
1387
- { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2) }
1474
+ {
1475
+ file: hooksFile2(env),
1476
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1477
+ note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1478
+ }
1388
1479
  ];
1389
1480
  let text = "";
1390
1481
  try {
@@ -1572,6 +1663,7 @@ function codexFacts(file) {
1572
1663
  }
1573
1664
 
1574
1665
  // ../core/src/adapters/gemini-cli.ts
1666
+ import { execFileSync as execFileSync3 } from "node:child_process";
1575
1667
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1576
1668
  import path7 from "node:path";
1577
1669
  var NAME4 = "gemini-cli";
@@ -1589,17 +1681,61 @@ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1589
1681
  var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1590
1682
  var settings = (env) => path7.join(geminiDir(env), "settings.json");
1591
1683
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1684
+ var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1685
+ function geminiHooksActive(settingsFile) {
1686
+ let tools;
1687
+ try {
1688
+ tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1689
+ } catch {
1690
+ }
1691
+ if (tools?.enableHooks === true) return { active: true };
1692
+ if (tools?.enableHooks === false)
1693
+ return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1694
+ let version = "";
1695
+ try {
1696
+ version = execFileSync3("gemini", ["--version"], {
1697
+ encoding: "utf8",
1698
+ timeout: 4e3,
1699
+ stdio: ["ignore", "pipe", "ignore"]
1700
+ }).trim();
1701
+ } catch {
1702
+ return {
1703
+ active: true,
1704
+ note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
1705
+ };
1706
+ }
1707
+ const m = /(\d+)\.(\d+)/.exec(version);
1708
+ if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1709
+ return {
1710
+ active: false,
1711
+ note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1712
+ };
1713
+ }
1592
1714
  var geminiCli = {
1593
1715
  name: NAME4,
1594
1716
  events: GEMINI_EVENTS,
1595
1717
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1596
1718
  detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1597
1719
  configFiles: (env = process.env) => [settings(env)],
1598
- install: (cmd, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3)],
1599
- uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1600
- installed: (cmd, env = process.env) => [
1601
- { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3) }
1720
+ install: (cmd, env = process.env, opts = {}) => {
1721
+ const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1722
+ const a = geminiHooksActive(settings(env));
1723
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1724
+ },
1725
+ uninstall: (env = process.env, opts = {}) => [
1726
+ uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1602
1727
  ],
1728
+ installed: (cmd, env = process.env, opts = {}) => {
1729
+ const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1730
+ const a = geminiHooksActive(settings(env));
1731
+ return [
1732
+ {
1733
+ file: settings(env),
1734
+ state: state === "current" && !a.active ? "inactive" : state,
1735
+ ...a.note ? { note: a.note } : {}
1736
+ }
1737
+ ];
1738
+ },
1603
1739
  fromHook(input) {
1604
1740
  const event = input.argv[1] ?? "";
1605
1741
  let s = {};
@@ -1810,7 +1946,7 @@ import {
1810
1946
  readSync as readSync2,
1811
1947
  renameSync,
1812
1948
  statSync as statSync4,
1813
- unlinkSync,
1949
+ unlinkSync as unlinkSync3,
1814
1950
  writeFileSync as writeFileSync5
1815
1951
  } from "node:fs";
1816
1952
  import path9 from "node:path";
@@ -1838,8 +1974,8 @@ var Outbox = class {
1838
1974
  } catch {
1839
1975
  n = this.lastSeqInFile(ref) + 1;
1840
1976
  }
1841
- mkdirSync5(path9.dirname(f), { recursive: true });
1842
- writeFileSync5(f, String(n + 1));
1977
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
1978
+ writeFileSync5(f, String(n + 1), { mode: 384 });
1843
1979
  return n;
1844
1980
  }
1845
1981
  lastSeqInFile(ref) {
@@ -1857,9 +1993,9 @@ var Outbox = class {
1857
1993
  }
1858
1994
  append(ref, events) {
1859
1995
  if (!events.length) return;
1860
- mkdirSync5(this.dir(ref.harness), { recursive: true });
1996
+ mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
1861
1997
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
1862
- `);
1998
+ `, { mode: 384 });
1863
1999
  }
1864
2000
  /** Every session file, newest first. */
1865
2001
  sessions() {
@@ -1923,9 +2059,9 @@ var Outbox = class {
1923
2059
  const f = this.file(ref);
1924
2060
  try {
1925
2061
  if (statSync4(f).mtimeMs < cutoff) {
1926
- unlinkSync(f);
2062
+ unlinkSync3(f);
1927
2063
  try {
1928
- unlinkSync(this.seqFile(ref));
2064
+ unlinkSync3(this.seqFile(ref));
1929
2065
  } catch {
1930
2066
  }
1931
2067
  n++;
@@ -1938,7 +2074,7 @@ var Outbox = class {
1938
2074
  delete(ref) {
1939
2075
  for (const f of [this.file(ref), this.seqFile(ref)]) {
1940
2076
  try {
1941
- unlinkSync(f);
2077
+ unlinkSync3(f);
1942
2078
  } catch {
1943
2079
  }
1944
2080
  }
@@ -1963,9 +2099,9 @@ var Cursors = class {
1963
2099
  }
1964
2100
  write(c) {
1965
2101
  const f = this.file();
1966
- mkdirSync5(path9.dirname(f), { recursive: true });
2102
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
1967
2103
  const tmp = `${f}.${process.pid}.tmp`;
1968
- writeFileSync5(tmp, JSON.stringify(c));
2104
+ writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
1969
2105
  renameSync(tmp, f);
1970
2106
  }
1971
2107
  key(ref) {
@@ -1974,10 +2110,10 @@ var Cursors = class {
1974
2110
  };
1975
2111
  function takeLock(root, ref, staleMs = 5 * 6e4) {
1976
2112
  const dir = path9.join(root, "locks");
1977
- mkdirSync5(dir, { recursive: true });
2113
+ mkdirSync5(dir, { recursive: true, mode: 448 });
1978
2114
  const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
1979
2115
  try {
1980
- if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync(f);
2116
+ if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync3(f);
1981
2117
  else return null;
1982
2118
  } catch {
1983
2119
  }
@@ -1988,7 +2124,7 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
1988
2124
  }
1989
2125
  return () => {
1990
2126
  try {
1991
- unlinkSync(f);
2127
+ unlinkSync3(f);
1992
2128
  } catch {
1993
2129
  }
1994
2130
  };
@@ -2124,7 +2260,7 @@ function redactAssignments(text) {
2124
2260
  function isSecretName(name) {
2125
2261
  if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
2126
2262
  const flat = name.toLowerCase().replace(/[_-]/g, "");
2127
- return /(?:pass(?:word|wd|phrase|code)?|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2263
+ return /(?:pass(?:word|wd|phrase|code)|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2128
2264
  flat
2129
2265
  );
2130
2266
  }
@@ -2213,7 +2349,7 @@ var DATA_ALLOW = {
2213
2349
  "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2214
2350
  "session.forgotten": { 0: [] },
2215
2351
  "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2216
- "turn.ended": { 0: ["turn_id", "reason", "ms"] },
2352
+ "turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
2217
2353
  "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2218
2354
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
2219
2355
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
@@ -2334,6 +2470,21 @@ var StdoutSink = class {
2334
2470
  }
2335
2471
  };
2336
2472
 
2473
+ // src/run.ts
2474
+ import {
2475
+ appendFileSync as appendFileSync3,
2476
+ existsSync as existsSync8,
2477
+ mkdirSync as mkdirSync7,
2478
+ readdirSync as readdirSync6,
2479
+ readFileSync as readFileSync8,
2480
+ renameSync as renameSync2,
2481
+ statSync as statSync5,
2482
+ unlinkSync as unlinkSync4,
2483
+ writeFileSync as writeFileSync6
2484
+ } from "node:fs";
2485
+ import os2 from "node:os";
2486
+ import path11 from "node:path";
2487
+
2337
2488
  // src/http-sink.ts
2338
2489
  import { createHmac, randomUUID } from "node:crypto";
2339
2490
  var TIMEOUT_MS = 15e3;
@@ -2420,9 +2571,41 @@ var HttpSink = class {
2420
2571
  };
2421
2572
 
2422
2573
  // src/run.ts
2574
+ function enqueueJob(state, job2) {
2575
+ const jobs = jobsDir(state);
2576
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
2577
+ const f = path11.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
2578
+ writeFileSync6(f, JSON.stringify(job2), { mode: 384 });
2579
+ return f;
2580
+ }
2581
+ async function sweepJobs(state, olderThanMs = 3e3, limit = 20) {
2582
+ const jobs = jobsDir(state);
2583
+ let names = [];
2584
+ try {
2585
+ names = readdirSync6(jobs).filter((n) => n.endsWith(".json"));
2586
+ } catch {
2587
+ return 0;
2588
+ }
2589
+ let ran = 0;
2590
+ for (const n of names.sort().slice(0, limit)) {
2591
+ const f = path11.join(jobs, n);
2592
+ let job2 = null;
2593
+ try {
2594
+ if (Date.now() - statSync5(f).mtimeMs < olderThanMs) continue;
2595
+ job2 = JSON.parse(readFileSync8(f, "utf8"));
2596
+ unlinkSync4(f);
2597
+ } catch {
2598
+ continue;
2599
+ }
2600
+ await runJob(job2, { state }).catch(() => {
2601
+ });
2602
+ ran++;
2603
+ }
2604
+ return ran;
2605
+ }
2423
2606
  function log(state, line) {
2424
2607
  try {
2425
- mkdirSync7(state, { recursive: true });
2608
+ mkdirSync7(state, { recursive: true, mode: 448 });
2426
2609
  const f = path11.join(state, "log");
2427
2610
  try {
2428
2611
  if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
@@ -2447,8 +2630,8 @@ function factsState(state) {
2447
2630
  const cur = this.get(h, s);
2448
2631
  const next = { ...cur, ...patch };
2449
2632
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
2450
- mkdirSync7(path11.dirname(file(h, s)), { recursive: true });
2451
- writeFileSync6(file(h, s), JSON.stringify(next));
2633
+ mkdirSync7(path11.dirname(file(h, s)), { recursive: true, mode: 448 });
2634
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
2452
2635
  }
2453
2636
  };
2454
2637
  }
@@ -2473,12 +2656,14 @@ async function runJob(job2, opts = {}) {
2473
2656
  if (!r || !r.session.id) return { events: [], delivered: {} };
2474
2657
  const outbox = new Outbox(state);
2475
2658
  const ref = { harness: job2.harness, session: r.session.id };
2476
- const release = takeLock(state, ref);
2659
+ let release = takeLock(state, ref);
2660
+ for (let wait = 50; !release && wait <= 3200; wait *= 2) {
2661
+ await new Promise((res) => setTimeout(res, wait));
2662
+ release = takeLock(state, ref);
2663
+ }
2477
2664
  if (!release) {
2478
- log(state, `${job2.harness} ${job2.event}: session ${r.session.id} locked, re-queued`);
2479
- const jobs = path11.join(state, "jobs");
2480
- mkdirSync7(jobs, { recursive: true });
2481
- writeFileSync6(path11.join(jobs, `${Date.now().toString(36)}-requeue.json`), JSON.stringify(job2));
2665
+ log(state, `${job2.harness} ${job2.event}: session ${r.session.id} locked for 6 s, re-queued`);
2666
+ enqueueJob(state, job2);
2482
2667
  return { events: [], delivered: {} };
2483
2668
  }
2484
2669
  try {
@@ -2542,7 +2727,10 @@ async function runJob(job2, opts = {}) {
2542
2727
  if (read.turns.length)
2543
2728
  facts.save(job2.harness, r.session.id, { transcript_line: read.line, transcript_turns: turnNo });
2544
2729
  }
2545
- outbox.append(ref, events);
2730
+ outbox.append(
2731
+ ref,
2732
+ events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
2733
+ );
2546
2734
  log(
2547
2735
  state,
2548
2736
  `${job2.harness} ${job2.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${r.session.id.slice(0, 8)})`
@@ -2568,34 +2756,57 @@ async function flush(state, sinkConfigs, outbox, first) {
2568
2756
  const cursors = new Cursors(state, sink.name);
2569
2757
  const c = cursors.read();
2570
2758
  let sent = 0;
2759
+ let stalled = false;
2760
+ let batch = [];
2761
+ let pending = {};
2762
+ const send = async () => {
2763
+ if (!batch.length) {
2764
+ Object.assign(c, pending);
2765
+ pending = {};
2766
+ cursors.write(c);
2767
+ return true;
2768
+ }
2769
+ const res = await sink.send(batch);
2770
+ if (!res.ok) {
2771
+ log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2772
+ if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2773
+ if (res.action !== "drop") {
2774
+ batch = [];
2775
+ pending = {};
2776
+ return false;
2777
+ }
2778
+ }
2779
+ sent += batch.length;
2780
+ Object.assign(c, pending);
2781
+ cursors.write(c);
2782
+ batch = [];
2783
+ pending = {};
2784
+ return true;
2785
+ };
2571
2786
  for (const ref of refs) {
2572
- if (sent >= MAX_PER_RUN) break;
2573
- let offset = c[cursors.key(ref)] ?? 0;
2574
- let stalled = false;
2575
- while (sent < MAX_PER_RUN) {
2576
- const { events, offset: next } = outbox.read(ref, offset, BATCH);
2787
+ if (stalled || sent >= MAX_PER_RUN) break;
2788
+ let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
2789
+ for (; ; ) {
2790
+ const room = BATCH - batch.length;
2791
+ const { events, offset: next } = outbox.read(ref, offset, room);
2577
2792
  if (!events.length) break;
2578
- const filtered = events.map((e) => filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() })).filter((e) => !!e);
2579
- let ok = true;
2580
- if (filtered.length) {
2581
- const res = await sink.send(filtered);
2582
- ok = res.ok;
2583
- if (!ok) {
2584
- log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2585
- if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2586
- if (res.action === "drop") ok = true;
2587
- }
2588
- if (ok) sent += filtered.length;
2589
- }
2590
- if (!ok) {
2591
- stalled = true;
2592
- break;
2793
+ for (const e of events) {
2794
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() });
2795
+ if (f) batch.push(f);
2593
2796
  }
2594
2797
  offset = next;
2595
- c[cursors.key(ref)] = offset;
2596
- cursors.write(c);
2798
+ pending[cursors.key(ref)] = offset;
2799
+ if (batch.length >= BATCH) {
2800
+ if (!await send()) {
2801
+ stalled = true;
2802
+ break;
2803
+ }
2804
+ if (sent >= MAX_PER_RUN) break;
2805
+ }
2597
2806
  }
2598
- if (stalled) break;
2807
+ }
2808
+ if (!stalled && (batch.length || Object.keys(pending).length)) {
2809
+ if (!await send()) stalled = true;
2599
2810
  }
2600
2811
  delivered[sink.name] = sent;
2601
2812
  }
@@ -2612,6 +2823,7 @@ function applySinkVerdict(name, res) {
2612
2823
  } catch {
2613
2824
  }
2614
2825
  }
2826
+ var jobsDir = (state) => path11.join(state, "jobs");
2615
2827
 
2616
2828
  // src/worker.ts
2617
2829
  var jobFile = process.argv[2];
@@ -2622,12 +2834,13 @@ try {
2622
2834
  } catch {
2623
2835
  }
2624
2836
  try {
2625
- unlinkSync2(jobFile);
2837
+ unlinkSync5(jobFile);
2626
2838
  } catch {
2627
2839
  }
2628
2840
  if (!job) process.exit(0);
2629
2841
  var deadline = setTimeout(() => process.exit(0), 15e3);
2630
2842
  deadline.unref();
2631
2843
  runJob(job).catch(() => {
2844
+ }).then(() => sweepJobs(stateDir())).catch(() => {
2632
2845
  }).finally(() => process.exit(0));
2633
2846
  //# sourceMappingURL=worker.js.map