sessionpipe 0.13.0 → 0.13.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.13.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.13.1 · Apache-2.0 · https://sessionpipe.org
2
2
  var __defProp = Object.defineProperty;
3
3
  var __export = (target, all) => {
4
4
  for (var name in all)
@@ -104,16 +104,16 @@ var HttpSink = class {
104
104
  import {
105
105
  appendFileSync as appendFileSync3,
106
106
  existsSync as existsSync10,
107
- mkdirSync as mkdirSync8,
107
+ mkdirSync as mkdirSync7,
108
108
  readdirSync as readdirSync7,
109
- readFileSync as readFileSync11,
110
- renameSync as renameSync3,
109
+ readFileSync as readFileSync12,
110
+ renameSync as renameSync4,
111
111
  statSync as statSync7,
112
- unlinkSync as unlinkSync4,
113
- writeFileSync as writeFileSync8
112
+ unlinkSync as unlinkSync3,
113
+ writeFileSync as writeFileSync6
114
114
  } from "node:fs";
115
115
  import os4 from "node:os";
116
- import path16 from "node:path";
116
+ import path17 from "node:path";
117
117
 
118
118
  // ../core/src/paths.ts
119
119
  import os from "node:os";
@@ -141,7 +141,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
141
141
  // ../core/src/adapters/claude-code.ts
142
142
  import { execFileSync as execFileSync2 } from "node:child_process";
143
143
  import { createHash as createHash2 } from "node:crypto";
144
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
144
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
145
145
  import { userInfo } from "node:os";
146
146
  import path4 from "node:path";
147
147
 
@@ -499,6 +499,41 @@ function readJson(file) {
499
499
  const f = readJsonFile(file);
500
500
  return f.ok ? f.value : {};
501
501
  }
502
+ function refused(file, e, what, existed) {
503
+ const { code, syscall } = e;
504
+ const why = `can't ${what} it (${code ?? String(e)})`;
505
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
506
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
507
+ return {
508
+ file,
509
+ changed: true,
510
+ skipped: true,
511
+ ...existed ? {} : { created: true },
512
+ note: `${why} part way; check the file before the harness reads it`
513
+ };
514
+ }
515
+ function writeBack(file, text, existed = true) {
516
+ try {
517
+ mkdirSync(path3.dirname(file), { recursive: true });
518
+ writeFileSync(file, text);
519
+ return null;
520
+ } catch (e) {
521
+ return refused(file, e, "write", existed);
522
+ }
523
+ }
524
+ function removeEmptied(file, empty) {
525
+ try {
526
+ unlinkSync(file);
527
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
528
+ } catch (e) {
529
+ const bad = refused(file, e, "remove", true);
530
+ return writeBack(file, empty) ?? {
531
+ file,
532
+ changed: true,
533
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
534
+ };
535
+ }
536
+ }
502
537
  function entryFor(event, harness, cmd, o) {
503
538
  const matcher = o.matcherFor(event);
504
539
  return {
@@ -540,8 +575,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
540
575
  }
541
576
  if (changed) {
542
577
  settings2.hooks = hooks;
543
- mkdirSync(path3.dirname(file), { recursive: true });
544
- writeFileSync(file, formatJson(settings2, f));
578
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
579
+ if (bad) return bad;
545
580
  }
546
581
  return { file, changed, created: !f.existed && changed };
547
582
  }
@@ -564,12 +599,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
564
599
  }
565
600
  if (!changed) return { file, changed: false };
566
601
  if (!Object.keys(hooks).length) delete settings2.hooks;
567
- if (opts.created && !Object.keys(settings2).length) {
568
- unlinkSync(file);
569
- return { file, changed: true, note: "removed (sessionpipe created it)" };
570
- }
571
- writeFileSync(file, formatJson(settings2, f));
572
- return { file, changed: true };
602
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
603
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
573
604
  }
574
605
  function installedClaudeShaped(file, harness, events, cmd, o) {
575
606
  const f = readJsonFile(file);
@@ -648,7 +679,7 @@ var claudeCode = {
648
679
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
649
680
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
650
681
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
651
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
682
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
652
683
  return line ? [hooks, line] : [hooks];
653
684
  }),
654
685
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -656,7 +687,9 @@ var claudeCode = {
656
687
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
657
688
  created: opts.created?.includes(settingsIn(d))
658
689
  });
659
- return line && !hooks.changed ? line : hooks;
690
+ if (line?.changed && !line.skipped && hooks.skipped)
691
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
692
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
660
693
  }),
661
694
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
662
695
  file: settingsIn(d),
@@ -751,7 +784,8 @@ var claudeCode = {
751
784
  attention_id: call_id ?? permissionId(),
752
785
  kind: "permission",
753
786
  tool,
754
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
787
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
788
+ agent_id
755
789
  })
756
790
  );
757
791
  break;
@@ -760,7 +794,8 @@ var claudeCode = {
760
794
  ev("attention.needed", {
761
795
  attention_id: `notif-${Date.now()}`,
762
796
  kind: notifKind(s.notification_type),
763
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
797
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
798
+ agent_id
764
799
  })
765
800
  );
766
801
  break;
@@ -885,8 +920,11 @@ function statusLine(file, want, cmd) {
885
920
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
886
921
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
887
922
  else delete f.value.statusLine;
888
- writeFileSync2(file, formatJson(f.value, f));
889
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
923
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
924
+ file,
925
+ changed: true,
926
+ note: want ? "status line written (account limits)" : "status line removed"
927
+ };
890
928
  }
891
929
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
892
930
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1492,16 +1530,7 @@ function runHeadless(bin, args, o) {
1492
1530
  }
1493
1531
 
1494
1532
  // ../core/src/adapters/antigravity.ts
1495
- import {
1496
- existsSync as existsSync3,
1497
- mkdirSync as mkdirSync2,
1498
- readdirSync as readdirSync3,
1499
- readFileSync as readFileSync4,
1500
- realpathSync,
1501
- statSync as statSync4,
1502
- unlinkSync as unlinkSync2,
1503
- writeFileSync as writeFileSync3
1504
- } from "node:fs";
1533
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync4 } from "node:fs";
1505
1534
  import path6 from "node:path";
1506
1535
  var NAME2 = "antigravity";
1507
1536
  var HOOK_NAME = "sessionpipe";
@@ -1533,10 +1562,8 @@ var antigravity = {
1533
1562
  const e = entry(cmd);
1534
1563
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1535
1564
  hooks[HOOK_NAME] = e;
1536
- mkdirSync2(path6.dirname(file), { recursive: true });
1537
- writeFileSync3(file, formatJson(hooks, f));
1538
1565
  return [
1539
- {
1566
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
1540
1567
  file,
1541
1568
  changed: true,
1542
1569
  created: !f.existed,
@@ -1552,12 +1579,8 @@ var antigravity = {
1552
1579
  const hooks = f.value;
1553
1580
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1554
1581
  delete hooks[HOOK_NAME];
1555
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1556
- unlinkSync2(file);
1557
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1558
- }
1559
- writeFileSync3(file, formatJson(hooks, f));
1560
- return [{ file, changed: true }];
1582
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
1583
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
1561
1584
  },
1562
1585
  installed(cmd, env = process.env) {
1563
1586
  const file = hooksFile(env);
@@ -2290,7 +2313,7 @@ function parseResult(stdout) {
2290
2313
  var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
2291
2314
 
2292
2315
  // ../core/src/adapters/codex.ts
2293
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2316
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
2294
2317
  import path8 from "node:path";
2295
2318
 
2296
2319
  // ../core/src/adapters/codex-config.ts
@@ -2443,29 +2466,30 @@ var configFile2 = (env) => path8.join(codexHome(env), "config.toml");
2443
2466
  var sessionsDir = (env) => path8.join(codexHome(env), "sessions");
2444
2467
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2445
2468
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
2469
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
2446
2470
  function installNotify(cmd, env) {
2447
2471
  const file = configFile2(env);
2448
2472
  let text = "";
2473
+ let existed = true;
2449
2474
  try {
2450
2475
  text = readFileSync6(file, "utf8");
2451
- } catch {
2476
+ } catch (e) {
2477
+ existed = e.code !== "ENOENT";
2452
2478
  }
2453
2479
  const argv = cmd([NAME3, "notify"]).argv;
2454
2480
  const line = `notify = ${JSON.stringify(argv)}`;
2455
2481
  const n = readNotify(text);
2456
- const write = (next) => {
2457
- mkdirSync3(path8.dirname(file), { recursive: true });
2458
- writeFileSync4(file, next);
2482
+ const write = (next, note) => {
2483
+ const bad = writeBack(file, next, existed);
2484
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
2459
2485
  };
2460
2486
  if (n && !n.other) {
2461
2487
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
2462
2488
  return { file, changed: false, note: "notify current" };
2463
- write(withNotify(text, line));
2464
- return {
2465
- file,
2466
- changed: true,
2467
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
2468
- };
2489
+ return write(
2490
+ withNotify(text, line),
2491
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
2492
+ );
2469
2493
  }
2470
2494
  if (n?.other) {
2471
2495
  const prev = previousNotify(n.argv);
@@ -2474,20 +2498,20 @@ function installNotify(cmd, env) {
2474
2498
  if (JSON.stringify(prev) === JSON.stringify(argv))
2475
2499
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
2476
2500
  }
2477
- if (!prev || OURS2.test(JSON.stringify(prev))) {
2478
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
2479
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
2480
- }
2501
+ if (!prev || OURS2.test(JSON.stringify(prev)))
2502
+ return write(
2503
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
2504
+ "chained after Codex Computer Use (--previous-notify)"
2505
+ );
2481
2506
  }
2482
2507
  return {
2483
2508
  file,
2484
2509
  changed: false,
2485
2510
  skipped: true,
2486
- 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`
2511
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
2487
2512
  };
2488
2513
  }
2489
- write(withNotify(text, line));
2490
- return { file, changed: true, note: "notify set at the top level" };
2514
+ return write(withNotify(text, line), "notify set at the top level");
2491
2515
  }
2492
2516
  function uninstallNotify(env) {
2493
2517
  const file = configFile2(env);
@@ -2499,15 +2523,13 @@ function uninstallNotify(env) {
2499
2523
  }
2500
2524
  const n = readNotify(text);
2501
2525
  if (!n) return { file, changed: false };
2502
- if (!n.other) {
2503
- writeFileSync4(file, withoutNotify(text));
2504
- return { file, changed: true };
2505
- }
2526
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
2506
2527
  const prev = previousNotify(n.argv);
2507
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
2508
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
2509
- return { file, changed: true };
2510
- }
2528
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
2529
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
2530
+ file,
2531
+ changed: true
2532
+ };
2511
2533
  return { file, changed: false };
2512
2534
  }
2513
2535
  function findRollout(threadId, env = process.env) {
@@ -2539,8 +2561,12 @@ function rolloutMeta(file) {
2539
2561
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
2540
2562
  }
2541
2563
  function ofAnotherThread(file, id) {
2564
+ const other = threadOf(file);
2565
+ return !!other && other !== id.toLowerCase();
2566
+ }
2567
+ function threadOf(file) {
2542
2568
  const m = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i.exec(file);
2543
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
2569
+ return m?.[1] ? m[1].toLowerCase() : null;
2544
2570
  }
2545
2571
  var codex = {
2546
2572
  name: NAME3,
@@ -2619,6 +2645,8 @@ var codex = {
2619
2645
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
2620
2646
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
2621
2647
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2648
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
2649
+ const agent_id = typeof s.agent_id === "string" && s.agent_id ? s.agent_id : typeof s.subagent_id === "string" && s.subagent_id ? s.subagent_id : thread && thread !== String(id).toLowerCase() ? thread : void 0;
2622
2650
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
2623
2651
  const events = [];
2624
2652
  switch (event) {
@@ -2638,17 +2666,30 @@ var codex = {
2638
2666
  );
2639
2667
  break;
2640
2668
  case "PreToolUse":
2641
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2669
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
2642
2670
  break;
2643
2671
  case "PostToolUse":
2644
2672
  if (tool)
2645
2673
  events.push(
2646
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
2674
+ ev("tool.ended", {
2675
+ tool,
2676
+ call_id,
2677
+ turn_id,
2678
+ agent_id,
2679
+ ok: true,
2680
+ input: s.tool_input,
2681
+ output: s.tool_response
2682
+ })
2647
2683
  );
2648
2684
  break;
2649
2685
  case "PermissionRequest":
2650
2686
  events.push(
2651
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
2687
+ ev("attention.needed", {
2688
+ attention_id: call_id ?? `perm-${Date.now()}`,
2689
+ kind: "permission",
2690
+ tool,
2691
+ agent_id
2692
+ })
2652
2693
  );
2653
2694
  break;
2654
2695
  case "Stop":
@@ -3138,7 +3179,7 @@ var CHAIN_LIMIT = 4e3;
3138
3179
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
3139
3180
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
3140
3181
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
3141
- UNION ALL
3182
+ UNION
3142
3183
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
3143
3184
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
3144
3185
  )
@@ -3407,7 +3448,7 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
3407
3448
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
3408
3449
 
3409
3450
  // ../core/src/config.ts
3410
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
3451
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
3411
3452
  import path11 from "node:path";
3412
3453
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
3413
3454
  function readConfig(file = configFile()) {
@@ -3423,9 +3464,36 @@ function readConfig(file = configFile()) {
3423
3464
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
3424
3465
  }
3425
3466
  }
3467
+ function configProblem(file) {
3468
+ let text;
3469
+ try {
3470
+ text = readFileSync8(file, "utf8");
3471
+ } catch (e) {
3472
+ const code = e.code;
3473
+ return code === "ENOENT" ? void 0 : `could not be read (${code})`;
3474
+ }
3475
+ if (!text.trim()) return void 0;
3476
+ if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
3477
+ try {
3478
+ const j = JSON.parse(text);
3479
+ return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
3480
+ } catch (e) {
3481
+ const m = e.message;
3482
+ const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
3483
+ return `is not valid JSON${at ? ` at ${at}` : ""}`;
3484
+ }
3485
+ }
3486
+ function assertConfigWritable(file = configFile()) {
3487
+ const problem = configProblem(file);
3488
+ if (problem)
3489
+ throw new Error(
3490
+ `${tilde(file)} ${problem}, so it was not written over: that would lose its machine name and sinks. Fix it, or move it aside to start again, then run this again.`
3491
+ );
3492
+ }
3426
3493
  function writeConfig(c, file = configFile()) {
3427
- mkdirSync4(path11.dirname(file), { recursive: true, mode: 448 });
3428
- writeFileSync5(file, `${JSON.stringify(c, null, 2)}
3494
+ assertConfigWritable(file);
3495
+ mkdirSync2(path11.dirname(file), { recursive: true, mode: 448 });
3496
+ writeFileSync2(file, `${JSON.stringify(c, null, 2)}
3429
3497
  `, { mode: 384 });
3430
3498
  if (process.platform !== "win32" && existsSync8(file)) {
3431
3499
  try {
@@ -3489,19 +3557,20 @@ function makeEvent(a, ctx) {
3489
3557
  }
3490
3558
 
3491
3559
  // ../core/src/outbox.ts
3560
+ import { randomBytes as randomBytes2 } from "node:crypto";
3492
3561
  import {
3493
3562
  appendFileSync,
3494
3563
  closeSync as closeSync3,
3495
3564
  existsSync as existsSync9,
3496
- mkdirSync as mkdirSync5,
3565
+ mkdirSync as mkdirSync3,
3497
3566
  openSync as openSync3,
3498
3567
  readdirSync as readdirSync6,
3499
3568
  readFileSync as readFileSync9,
3500
3569
  readSync as readSync3,
3501
3570
  renameSync,
3502
3571
  statSync as statSync6,
3503
- unlinkSync as unlinkSync3,
3504
- writeFileSync as writeFileSync6
3572
+ unlinkSync as unlinkSync2,
3573
+ writeFileSync as writeFileSync3
3505
3574
  } from "node:fs";
3506
3575
  import path12 from "node:path";
3507
3576
  var READ_CHUNK = 1024 * 1024;
@@ -3531,8 +3600,8 @@ var Outbox = class {
3531
3600
  } catch {
3532
3601
  }
3533
3602
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
3534
- mkdirSync5(path12.dirname(f), { recursive: true, mode: 448 });
3535
- writeFileSync6(f, String(n + 1), { mode: 384 });
3603
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3604
+ writeFileSync3(f, String(n + 1), { mode: 384 });
3536
3605
  return n;
3537
3606
  }
3538
3607
  lastSeqInFile(ref) {
@@ -3550,7 +3619,7 @@ var Outbox = class {
3550
3619
  }
3551
3620
  append(ref, events) {
3552
3621
  if (!events.length) return;
3553
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
3622
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
3554
3623
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
3555
3624
  `, { mode: 384 });
3556
3625
  }
@@ -3620,34 +3689,106 @@ var Outbox = class {
3620
3689
  closeSync3(fd);
3621
3690
  }
3622
3691
  }
3623
- /** Files older than `days` are pruned; the cursor files with them. */
3692
+ /** Files older than `days` are pruned, and each sink's cursor into them. The seq
3693
+ * counter stays: a session resumed after the window counts on from where it was,
3694
+ * so a receiver's (harness, session, seq) dedup doesn't take its new events for
3695
+ * ones it already has, and the sinks read the new file from its start.
3696
+ * It all happens under the drain's `flush` lock, since a drain holds the whole
3697
+ * cursor map and writes it back: a file and its cursor keys go together, so a
3698
+ * session resumed in between can't keep a key past its new file's end. While a
3699
+ * drain runs, nothing is pruned (0); the next prune does it. It never throws: it
3700
+ * is a cleanup, and a hook's drain comes after it. */
3624
3701
  prune(days, now = Date.now()) {
3702
+ let release = null;
3703
+ try {
3704
+ release = takeNamedLock(this.root, "flush");
3705
+ } catch {
3706
+ }
3707
+ if (!release) return 0;
3625
3708
  let n = 0;
3626
- const cutoff = now - days * 864e5;
3627
- for (const ref of this.sessions()) {
3628
- const f = this.file(ref);
3629
- try {
3630
- if (statSync6(f).mtimeMs < cutoff) {
3631
- unlinkSync3(f);
3632
- try {
3633
- unlinkSync3(this.seqFile(ref));
3634
- } catch {
3709
+ try {
3710
+ const cutoff = now - days * 864e5;
3711
+ const aged = this.sessions().filter((ref) => {
3712
+ try {
3713
+ return statSync6(this.file(ref)).mtimeMs < cutoff;
3714
+ } catch {
3715
+ return false;
3716
+ }
3717
+ });
3718
+ this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
3719
+ for (const ref of aged) {
3720
+ const f = this.file(ref);
3721
+ try {
3722
+ if (statSync6(f).mtimeMs < cutoff) {
3723
+ unlinkSync2(f);
3724
+ n++;
3635
3725
  }
3636
- n++;
3726
+ } catch {
3637
3727
  }
3638
- } catch {
3639
3728
  }
3729
+ } catch {
3730
+ } finally {
3731
+ release();
3640
3732
  }
3641
3733
  return n;
3642
3734
  }
3735
+ /** A sink's cursor into a session file about to be pruned (`gone`), or one already
3736
+ * missing (an earlier prune, an older release, by hand) is removed. The caller
3737
+ * holds the `flush` lock. */
3738
+ dropStaleCursors(gone) {
3739
+ let names = [];
3740
+ try {
3741
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3742
+ } catch {
3743
+ }
3744
+ const stale = (c) => Object.keys(c).filter((k) => {
3745
+ if (gone.has(k)) return true;
3746
+ const i = k.indexOf("/");
3747
+ return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
3748
+ });
3749
+ for (const n of names) {
3750
+ const s = new Cursors(this.root, n.slice(0, -5));
3751
+ const c = s.read();
3752
+ const keys = stale(c);
3753
+ if (!keys.length) continue;
3754
+ for (const k of keys) delete c[k];
3755
+ s.write(c);
3756
+ }
3757
+ }
3643
3758
  delete(ref) {
3759
+ this.dropCursors(ref);
3644
3760
  for (const f of [this.file(ref), this.seqFile(ref)]) {
3645
3761
  try {
3646
- unlinkSync3(f);
3762
+ unlinkSync2(f);
3647
3763
  } catch {
3648
3764
  }
3649
3765
  }
3650
3766
  }
3767
+ /** The key every sink's cursor holds for a session is removed with the session's
3768
+ * file: the key alone pointed into the old file, and a session that went on (still
3769
+ * running, or resumed later) had its new file's first events unsent until it grew
3770
+ * past that offset. Every file under <state>/cursors is taken (a sink since taken
3771
+ * out of the config still has one; one that doesn't parse reads as empty and is
3772
+ * left), by the key a drain writes (the file's own name, `safeId`) and by the ref
3773
+ * as given. The caller holds the drain's `flush` lock: a drain holds the whole
3774
+ * cursor map and writes it back, so a key dropped under it would come back. */
3775
+ dropCursors(ref) {
3776
+ const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
3777
+ let names = [];
3778
+ try {
3779
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3780
+ } catch {
3781
+ return;
3782
+ }
3783
+ for (const n of names) {
3784
+ const s = new Cursors(this.root, n.slice(0, -5));
3785
+ const c = s.read();
3786
+ const had = Object.keys(c).filter((k) => keys.has(k));
3787
+ if (!had.length) continue;
3788
+ for (const k of had) delete c[k];
3789
+ s.write(c);
3790
+ }
3791
+ }
3651
3792
  };
3652
3793
  var Cursors = class {
3653
3794
  constructor(root, sink) {
@@ -3659,18 +3800,21 @@ var Cursors = class {
3659
3800
  file() {
3660
3801
  return path12.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3661
3802
  }
3803
+ /** The map; empty when the file is missing, cut off mid-write, or holds anything
3804
+ * but an object (`null`, a list), so a drain never indexes into those. */
3662
3805
  read() {
3663
3806
  try {
3664
- return JSON.parse(readFileSync9(this.file(), "utf8"));
3807
+ const c = JSON.parse(readFileSync9(this.file(), "utf8"));
3808
+ return c && typeof c === "object" && !Array.isArray(c) ? c : {};
3665
3809
  } catch {
3666
3810
  return {};
3667
3811
  }
3668
3812
  }
3669
3813
  write(c) {
3670
3814
  const f = this.file();
3671
- mkdirSync5(path12.dirname(f), { recursive: true, mode: 448 });
3815
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3672
3816
  const tmp = `${f}.${process.pid}.tmp`;
3673
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
3817
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
3674
3818
  renameSync(tmp, f);
3675
3819
  }
3676
3820
  key(ref) {
@@ -3682,21 +3826,43 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
3682
3826
  }
3683
3827
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
3684
3828
  const dir = path12.join(root, "locks");
3685
- mkdirSync5(dir, { recursive: true, mode: 448 });
3829
+ mkdirSync3(dir, { recursive: true, mode: 448 });
3686
3830
  const f = path12.join(dir, `${name}.lock`);
3831
+ const nonce = randomBytes2(8).toString("hex");
3832
+ const mine = `${process.pid} ${nonce}`;
3833
+ let body = null;
3687
3834
  try {
3688
- if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync9(f, "utf8")))) unlinkSync3(f);
3689
- else return null;
3835
+ body = readFileSync9(f, "utf8");
3836
+ const stale = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
3837
+ if (!stale) return null;
3690
3838
  } catch {
3839
+ body = null;
3840
+ }
3841
+ if (body !== null) {
3842
+ const aside = `${f}.${process.pid}.${nonce}.stale`;
3843
+ try {
3844
+ renameSync(f, aside);
3845
+ } catch {
3846
+ return null;
3847
+ }
3848
+ const moved = readFileSync9(aside, "utf8");
3849
+ unlinkSync2(aside);
3850
+ if (moved !== body) {
3851
+ try {
3852
+ writeFileSync3(f, moved, { flag: "wx" });
3853
+ } catch {
3854
+ }
3855
+ return null;
3856
+ }
3691
3857
  }
3692
3858
  try {
3693
- writeFileSync6(f, String(process.pid), { flag: "wx" });
3859
+ writeFileSync3(f, mine, { flag: "wx" });
3694
3860
  } catch {
3695
3861
  return null;
3696
3862
  }
3697
3863
  return () => {
3698
3864
  try {
3699
- unlinkSync3(f);
3865
+ if (readFileSync9(f, "utf8") === mine) unlinkSync2(f);
3700
3866
  } catch {
3701
3867
  }
3702
3868
  };
@@ -3715,7 +3881,7 @@ function processAlive(pid) {
3715
3881
  import { createHash as createHash4 } from "node:crypto";
3716
3882
  var RULESET_PII = "pii@1";
3717
3883
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
3718
- var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
3884
+ var PHONE = /(?<![\w.])(?:(?<!\)[ .-]?|\+\d{1,3}[ .-]?)(\+?(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))(?:(?= (?:[01]?\d|2[0-3]):)|(?!\w|[ .-]\d\d))|(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w))/g;
3719
3885
  function looksPhone(s) {
3720
3886
  const digits = s.replace(/\D/g, "");
3721
3887
  if (digits.length < 7 || digits.length > 15) return false;
@@ -3726,7 +3892,7 @@ function looksPhone(s) {
3726
3892
  function reducePii(text, home) {
3727
3893
  if (!text) return text;
3728
3894
  let out = text.replace(EMAIL, "[email]");
3729
- out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
3895
+ out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
3730
3896
  if (home) {
3731
3897
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3732
3898
  out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -3940,7 +4106,7 @@ var DATA_ALLOW = {
3940
4106
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
3941
4107
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
3942
4108
  "files.changed": { 1: ["paths"] },
3943
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
4109
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
3944
4110
  "attention.cleared": { 0: ["attention_id", "how"] },
3945
4111
  "subagent.started": { 1: ["agent_id", "agent_type"] },
3946
4112
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
@@ -4031,7 +4197,7 @@ function shortId(harness, id) {
4031
4197
  }
4032
4198
 
4033
4199
  // ../core/src/sinks/file.ts
4034
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
4200
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
4035
4201
  import path13 from "node:path";
4036
4202
  var FileSink = class {
4037
4203
  constructor(name, file, tier, pii) {
@@ -4046,7 +4212,7 @@ var FileSink = class {
4046
4212
  pii;
4047
4213
  async send(events) {
4048
4214
  try {
4049
- mkdirSync6(path13.dirname(this.file), { recursive: true });
4215
+ mkdirSync4(path13.dirname(this.file), { recursive: true });
4050
4216
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
4051
4217
  `);
4052
4218
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -4080,12 +4246,12 @@ var StdoutSink = class {
4080
4246
  };
4081
4247
 
4082
4248
  // src/control/aliases.ts
4083
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
4249
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
4084
4250
  import path14 from "node:path";
4085
4251
 
4086
4252
  // src/secrets.ts
4087
4253
  import { spawnSync } from "node:child_process";
4088
- import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
4254
+ import { createHash as createHash6, randomBytes as randomBytes3 } from "node:crypto";
4089
4255
  var SERVICE = "sessionpipe";
4090
4256
  var defaultRunner = (cmd, args, input) => {
4091
4257
  const r = spawnSync(cmd, args, {
@@ -4109,9 +4275,9 @@ function secretGet(store, account, run = defaultRunner) {
4109
4275
  function sinkWithSecrets(s, run = defaultRunner) {
4110
4276
  if (!s.token_in) return s;
4111
4277
  const token = secretGet(s.token_in, sinkAccount(s.name), run);
4112
- if (!token) return null;
4113
4278
  const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
4114
- return { ...s, token, ...secret ? { secret } : {} };
4279
+ if (!token && !secret) return null;
4280
+ return { ...s, ...token ? { token } : {}, ...secret ? { secret } : {} };
4115
4281
  }
4116
4282
 
4117
4283
  // src/control/store.ts
@@ -4186,6 +4352,76 @@ function ask(sock, req, timeoutMs) {
4186
4352
  });
4187
4353
  }
4188
4354
 
4355
+ // src/fold.ts
4356
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
4357
+ import path16 from "node:path";
4358
+ var TOOLS_MODES = ["all", "main", "beat"];
4359
+ var TOOL_FOLD_MS = 6e4;
4360
+ var KEEP_MS = 24 * 60 * 6e4;
4361
+ function isToolsMode(v) {
4362
+ return typeof v === "string" && TOOLS_MODES.includes(v);
4363
+ }
4364
+ function toolsMode(s) {
4365
+ if (isToolsMode(s.tools)) return s.tools;
4366
+ if (isToolsMode(s.receiver_tools)) return s.receiver_tools;
4367
+ return "all";
4368
+ }
4369
+ var agentOf = (e) => typeof e.data.agent_id === "string" ? e.data.agent_id : "";
4370
+ var thins = (mode, agent) => mode === "beat" || mode === "main" && agent !== "";
4371
+ var askKey = (key, agent) => `${key}\0ask\0${agent}`;
4372
+ function sendTool(e, mode, clock, key) {
4373
+ if (mode === "all") return true;
4374
+ const agent = agentOf(e);
4375
+ const t = Date.parse(e.time);
4376
+ if (e.type === "attention.needed") {
4377
+ if (thins(mode, agent)) clock[askKey(key, agent)] = Number.isFinite(t) ? t : 0;
4378
+ return true;
4379
+ }
4380
+ if (e.type !== "tool.started" && e.type !== "tool.ended" || !thins(mode, agent)) return true;
4381
+ if (e.type === "tool.started") return false;
4382
+ if (!Number.isFinite(t)) return true;
4383
+ const ask2 = askKey(key, agent);
4384
+ const asked = clock[ask2];
4385
+ if (asked !== void 0 && t >= asked) {
4386
+ delete clock[ask2];
4387
+ clock[key] = t;
4388
+ return true;
4389
+ }
4390
+ const last = clock[key];
4391
+ if (last !== void 0 && t > last - TOOL_FOLD_MS && t < last + TOOL_FOLD_MS) return false;
4392
+ clock[key] = t;
4393
+ return true;
4394
+ }
4395
+ var ToolClock = class {
4396
+ constructor(root, sink) {
4397
+ this.root = root;
4398
+ this.sink = sink;
4399
+ }
4400
+ root;
4401
+ sink;
4402
+ file() {
4403
+ return path16.join(this.root, "folds", `${safeId(this.sink)}.json`);
4404
+ }
4405
+ read() {
4406
+ try {
4407
+ const j = JSON.parse(readFileSync11(this.file(), "utf8"));
4408
+ return j && typeof j === "object" && !Array.isArray(j) ? j : {};
4409
+ } catch {
4410
+ return {};
4411
+ }
4412
+ }
4413
+ write(c) {
4414
+ const newest = Math.max(0, ...Object.values(c).filter(Number.isFinite));
4415
+ const kept = {};
4416
+ for (const [k, v] of Object.entries(c)) if (Number.isFinite(v) && v >= newest - KEEP_MS) kept[k] = v;
4417
+ const f = this.file();
4418
+ mkdirSync6(path16.dirname(f), { recursive: true, mode: 448 });
4419
+ const tmp = `${f}.${process.pid}.tmp`;
4420
+ writeFileSync5(tmp, JSON.stringify(kept), { mode: 384 });
4421
+ renameSync3(tmp, f);
4422
+ }
4423
+ };
4424
+
4189
4425
  // src/run.ts
4190
4426
  async function tellDaemon(state, job, session, cwd, transcript) {
4191
4427
  const sock = socketPath(state);
@@ -4210,24 +4446,24 @@ async function tellDaemon(state, job, session, cwd, transcript) {
4210
4446
  }
4211
4447
  function enqueueJob(state, job) {
4212
4448
  const jobs = jobsDir(state);
4213
- mkdirSync8(jobs, { recursive: true, mode: 448 });
4214
- const f = path16.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4215
- writeFileSync8(f, JSON.stringify(job), { mode: 384 });
4449
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
4450
+ const f = path17.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4451
+ writeFileSync6(f, JSON.stringify(job), { mode: 384 });
4216
4452
  return f;
4217
4453
  }
4218
4454
  function releaseJob(claimed) {
4219
4455
  if (!claimed) return;
4220
4456
  try {
4221
- unlinkSync4(claimed);
4457
+ unlinkSync3(claimed);
4222
4458
  } catch {
4223
4459
  }
4224
4460
  }
4225
4461
  function log(state, line) {
4226
4462
  try {
4227
- mkdirSync8(state, { recursive: true, mode: 448 });
4228
- const f = path16.join(state, "log");
4463
+ mkdirSync7(state, { recursive: true, mode: 448 });
4464
+ const f = path17.join(state, "log");
4229
4465
  try {
4230
- if (statSync7(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
4466
+ if (statSync7(f).size > 1024 * 1024) renameSync4(f, `${f}.1`);
4231
4467
  } catch {
4232
4468
  }
4233
4469
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -4236,11 +4472,11 @@ function log(state, line) {
4236
4472
  }
4237
4473
  }
4238
4474
  function factsState(state) {
4239
- const file = (h, s) => path16.join(state, "facts", safeId(h), `${safeId(s)}.json`);
4475
+ const file = (h, s) => path17.join(state, "facts", safeId(h), `${safeId(s)}.json`);
4240
4476
  return {
4241
4477
  get(h, s) {
4242
4478
  try {
4243
- return JSON.parse(readFileSync11(file(h, s), "utf8"));
4479
+ return JSON.parse(readFileSync12(file(h, s), "utf8"));
4244
4480
  } catch {
4245
4481
  return {};
4246
4482
  }
@@ -4249,8 +4485,8 @@ function factsState(state) {
4249
4485
  const cur = this.get(h, s);
4250
4486
  const next = { ...cur, ...patch };
4251
4487
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
4252
- mkdirSync8(path16.dirname(file(h, s)), { recursive: true, mode: 448 });
4253
- writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
4488
+ mkdirSync7(path17.dirname(file(h, s)), { recursive: true, mode: 448 });
4489
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
4254
4490
  }
4255
4491
  };
4256
4492
  }
@@ -4263,7 +4499,7 @@ function buildSinks(sinks, o = {}) {
4263
4499
  o.locked?.(raw.name);
4264
4500
  continue;
4265
4501
  }
4266
- const tier = Math.min(s.tier, s.max_tier ?? 3);
4502
+ const tier = Math.trunc(Math.min(Number(s.tier) || 0, s.max_tier ?? 3));
4267
4503
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
4268
4504
  else if (s.url.startsWith("file:"))
4269
4505
  out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os4.homedir()), tier, !!s.pii));
@@ -4325,7 +4561,7 @@ async function runJob(job, opts = {}) {
4325
4561
  );
4326
4562
  return { events: [], delivered: {} };
4327
4563
  }
4328
- const id = receiverIdOf(readAliases(aliasFile(path16.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
4564
+ const id = receiverIdOf(readAliases(aliasFile(path17.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
4329
4565
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
4330
4566
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
4331
4567
  const outbox = new Outbox(state);
@@ -4362,7 +4598,7 @@ async function runJob(job, opts = {}) {
4362
4598
  learned = rest;
4363
4599
  if (harness_version) facts.save(job.harness, id, { harness_version });
4364
4600
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
4365
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path16.isAbsolute(r.session.cwd))
4601
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path17.isAbsolute(r.session.cwd))
4366
4602
  facts.save(job.harness, id, { cwd: r.session.cwd });
4367
4603
  }
4368
4604
  const known = mem.session ?? {};
@@ -4480,29 +4716,83 @@ async function runJob(job, opts = {}) {
4480
4716
  } finally {
4481
4717
  release();
4482
4718
  }
4483
- const delivered = await drain(state, cfg.sinks, outbox, ref) ?? {};
4484
4719
  if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
4720
+ const delivered = opts.drain === false ? {} : await drain(state, cfg.sinks, outbox, ref, {
4721
+ ...opts.clock ? { clock: opts.clock } : {},
4722
+ ...opts.deadline !== void 0 ? { deadline: opts.deadline } : {},
4723
+ ...opts.handoff ? { handoff: opts.handoff } : {}
4724
+ }) ?? {};
4485
4725
  return { events, delivered };
4486
4726
  }
4487
- async function drain(state, sinkConfigs, outbox, first) {
4488
- const more = path16.join(state, "locks", "flush.more");
4727
+ var REAL_CLOCK = { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
4728
+ var PACE_MS = 1e3;
4729
+ var sentFile = (state) => path17.join(state, "locks", "flush.sent");
4730
+ function lastSent(state) {
4731
+ try {
4732
+ return Number(readFileSync12(sentFile(state), "utf8"));
4733
+ } catch {
4734
+ return 0;
4735
+ }
4736
+ }
4737
+ function noteSent(state, at) {
4738
+ try {
4739
+ mkdirSync7(path17.dirname(sentFile(state)), { recursive: true, mode: 448 });
4740
+ writeFileSync6(sentFile(state), String(at), { mode: 384 });
4741
+ } catch {
4742
+ }
4743
+ }
4744
+ function waitingForReceiver(state, sinkConfigs, outbox, now) {
4745
+ const receivers = sinkConfigs.filter(
4746
+ (s) => !s.paused && /^https?:\/\//.test(s.url) && !((readBackoff(state, s.name)?.until ?? 0) > now)
4747
+ );
4748
+ if (!receivers.length) return false;
4749
+ const refs = outbox.sessions();
4750
+ for (const s of receivers) {
4751
+ const cursors = new Cursors(state, s.name);
4752
+ const c = cursors.read();
4753
+ for (const ref of refs) {
4754
+ try {
4755
+ if (statSync7(outbox.file(ref)).size > (c[cursors.key(ref)] ?? 0)) return true;
4756
+ } catch {
4757
+ }
4758
+ }
4759
+ }
4760
+ return false;
4761
+ }
4762
+ var PASS_BUDGET_MS = 3e3;
4763
+ var moreFile = (state) => path17.join(state, "locks", "flush.more");
4764
+ function markMore(state) {
4765
+ try {
4766
+ mkdirSync7(path17.dirname(moreFile(state)), { recursive: true, mode: 448 });
4767
+ writeFileSync6(moreFile(state), "", { mode: 384 });
4768
+ } catch {
4769
+ }
4770
+ }
4771
+ async function drain(state, sinkConfigs, outbox, first, o = {}) {
4772
+ const clock = o.clock ?? REAL_CLOCK;
4773
+ const more = moreFile(state);
4489
4774
  let release = takeNamedLock(state, "flush");
4490
4775
  if (!release) {
4491
- try {
4492
- writeFileSync8(more, "", { mode: 384 });
4493
- } catch {
4494
- }
4776
+ markMore(state);
4495
4777
  release = takeNamedLock(state, "flush");
4496
4778
  if (!release) return null;
4497
4779
  }
4498
4780
  const delivered = {};
4499
4781
  for (; ; ) {
4500
- try {
4501
- unlinkSync4(more);
4502
- } catch {
4782
+ if (o.deadline !== void 0 && clock.now() + PASS_BUDGET_MS > o.deadline) {
4783
+ markMore(state);
4784
+ release();
4785
+ o.handoff?.();
4786
+ return delivered;
4503
4787
  }
4504
4788
  try {
4505
- for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first)))
4789
+ const wait = Math.min(PACE_MS, lastSent(state) + PACE_MS - clock.now());
4790
+ if (wait > 0 && waitingForReceiver(state, sinkConfigs, outbox, clock.now())) await clock.sleep(wait);
4791
+ try {
4792
+ unlinkSync3(more);
4793
+ } catch {
4794
+ }
4795
+ for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first, { clock })))
4506
4796
  delivered[k] = (delivered[k] ?? 0) + v;
4507
4797
  } finally {
4508
4798
  release();
@@ -4513,10 +4803,10 @@ async function drain(state, sinkConfigs, outbox, first) {
4513
4803
  }
4514
4804
  }
4515
4805
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
4516
- var backoffFile = (state, sink) => path16.join(state, "backoff", `${safeId(sink)}.json`);
4806
+ var backoffFile = (state, sink) => path17.join(state, "backoff", `${safeId(sink)}.json`);
4517
4807
  function readBackoff(state, sink) {
4518
4808
  try {
4519
- const b = JSON.parse(readFileSync11(backoffFile(state, sink), "utf8"));
4809
+ const b = JSON.parse(readFileSync12(backoffFile(state, sink), "utf8"));
4520
4810
  return typeof b.until === "number" ? b : null;
4521
4811
  } catch {
4522
4812
  return null;
@@ -4529,15 +4819,15 @@ function noteFailure(state, sink, res, prev) {
4529
4819
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
4530
4820
  const f = backoffFile(state, sink);
4531
4821
  try {
4532
- mkdirSync8(path16.dirname(f), { recursive: true, mode: 448 });
4533
- writeFileSync8(f, JSON.stringify(b), { mode: 384 });
4822
+ mkdirSync7(path17.dirname(f), { recursive: true, mode: 448 });
4823
+ writeFileSync6(f, JSON.stringify(b), { mode: 384 });
4534
4824
  } catch {
4535
4825
  }
4536
4826
  return b;
4537
4827
  }
4538
4828
  function clearBackoff(state, sink) {
4539
4829
  try {
4540
- unlinkSync4(backoffFile(state, sink));
4830
+ unlinkSync3(backoffFile(state, sink));
4541
4831
  } catch {
4542
4832
  }
4543
4833
  }
@@ -4547,14 +4837,31 @@ var BATCH = 50;
4547
4837
  var MAX_PER_RUN = 500;
4548
4838
  async function flush(state, sinkConfigs, outbox, first, o = {}) {
4549
4839
  const delivered = {};
4840
+ const clock = o.clock ?? REAL_CLOCK;
4550
4841
  const sinks = buildSinks(sinkConfigs, {
4551
4842
  locked: (name) => log(state, `sink ${name}: its token is in a keychain this process can't read; left for later`)
4552
4843
  });
4844
+ const modes = new Map(sinkConfigs.map((s) => [s.name, toolsMode(s)]));
4553
4845
  const refs = outbox.sessions();
4554
4846
  if (first) {
4555
4847
  const k = `${first.harness}/${safeId(first.session)}`;
4556
4848
  refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
4557
4849
  }
4850
+ const unreadable = /* @__PURE__ */ new Set();
4851
+ const readOrSkip = (ref, offset, room) => {
4852
+ try {
4853
+ return outbox.read(ref, offset, room);
4854
+ } catch (e) {
4855
+ const code = e.code;
4856
+ if (typeof code !== "string") throw e;
4857
+ const f = outbox.file(ref);
4858
+ if (!unreadable.has(f)) {
4859
+ unreadable.add(f);
4860
+ log(state, `drain: can't read ${f} (${code}); skipped, its cursors stay`);
4861
+ }
4862
+ return null;
4863
+ }
4864
+ };
4558
4865
  const drainSink = async (sink) => {
4559
4866
  let backoff = readBackoff(state, sink.name);
4560
4867
  if (backoff && !o.force && backoff.until > Date.now()) {
@@ -4563,11 +4870,16 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4563
4870
  }
4564
4871
  const cursors = new Cursors(state, sink.name);
4565
4872
  const c = cursors.read();
4873
+ const mode = modes.get(sink.name);
4874
+ const tools = new ToolClock(state, sink.name);
4875
+ let clockNow = mode === "all" ? {} : tools.read();
4876
+ let clockNext = { ...clockNow };
4566
4877
  let sent = 0;
4567
4878
  let stalled = false;
4568
4879
  let batch = [];
4569
4880
  let pending = {};
4570
4881
  const deliver = async (events) => {
4882
+ if (sink instanceof HttpSink) noteSent(state, clock.now());
4571
4883
  const res = await sink.send(events);
4572
4884
  if (res.ok || res.action !== "split") return res;
4573
4885
  if (events.length === 1) {
@@ -4575,10 +4887,20 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4575
4887
  log(state, `sink ${sink.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
4576
4888
  return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
4577
4889
  }
4890
+ const part = async (some) => {
4891
+ const r = await deliver(some);
4892
+ if (r.ok || r.action !== "drop") return r;
4893
+ log(
4894
+ state,
4895
+ `sink ${sink.name}: ${r.status ?? ""} ${r.error ?? ""} \u2192 drop, ${some.length} of ${events.length} events`
4896
+ );
4897
+ const rejected = some.map((e) => ({ id: e.id, reason: "dropped" }));
4898
+ return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected };
4899
+ };
4578
4900
  const half = Math.ceil(events.length / 2);
4579
- const a = await deliver(events.slice(0, half));
4901
+ const a = await part(events.slice(0, half));
4580
4902
  if (!a.ok) return a;
4581
- const b = await deliver(events.slice(half));
4903
+ const b = await part(events.slice(half));
4582
4904
  if (!b.ok) return b;
4583
4905
  return {
4584
4906
  ok: true,
@@ -4588,11 +4910,17 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4588
4910
  rejected: [...a.rejected ?? [], ...b.rejected ?? []]
4589
4911
  };
4590
4912
  };
4913
+ const commitClock = () => {
4914
+ if (mode === "all" || JSON.stringify(clockNext) === JSON.stringify(clockNow)) return;
4915
+ tools.write(clockNext);
4916
+ clockNow = { ...clockNext };
4917
+ };
4591
4918
  const send = async () => {
4592
4919
  if (!batch.length) {
4593
4920
  Object.assign(c, pending);
4594
4921
  pending = {};
4595
4922
  cursors.write(c);
4923
+ commitClock();
4596
4924
  return true;
4597
4925
  }
4598
4926
  const res = await deliver(batch);
@@ -4608,6 +4936,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4608
4936
  if (res.action !== "drop") {
4609
4937
  batch = [];
4610
4938
  pending = {};
4939
+ clockNext = { ...clockNow };
4611
4940
  return false;
4612
4941
  }
4613
4942
  } else if (backoff) {
@@ -4617,6 +4946,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4617
4946
  sent += batch.length;
4618
4947
  Object.assign(c, pending);
4619
4948
  cursors.write(c);
4949
+ commitClock();
4620
4950
  batch = [];
4621
4951
  pending = {};
4622
4952
  return true;
@@ -4626,11 +4956,15 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4626
4956
  let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
4627
4957
  for (; ; ) {
4628
4958
  const room = BATCH - batch.length;
4629
- const { events, offset: next } = outbox.read(ref, offset, room);
4959
+ const read = readOrSkip(ref, offset, room);
4960
+ if (!read) break;
4961
+ const { events, offset: next } = read;
4630
4962
  if (!events.length) break;
4631
4963
  for (const e of events) {
4632
4964
  const f = filterEvent(e, sink.tier, sink.pii, { home: os4.homedir() });
4633
- if (f) batch.push(f);
4965
+ if (!f) continue;
4966
+ if (sendTool(f, mode, clockNext, cursors.key(ref))) batch.push(f);
4967
+ else if (o.thinned) o.thinned[sink.name] = (o.thinned[sink.name] ?? 0) + 1;
4634
4968
  }
4635
4969
  offset = next;
4636
4970
  pending[cursors.key(ref)] = offset;
@@ -4662,7 +4996,7 @@ function applySinkVerdict(name, res) {
4662
4996
  } catch {
4663
4997
  }
4664
4998
  }
4665
- var jobsDir = (state) => path16.join(state, "jobs");
4999
+ var jobsDir = (state) => path17.join(state, "jobs");
4666
5000
  export {
4667
5001
  HttpSink,
4668
5002
  buildSinks,