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/worker.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.13.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.13.1 · Apache-2.0 · https://sessionpipe.org
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
5
5
  for (var name in all)
@@ -37,7 +37,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
37
37
  // ../core/src/adapters/claude-code.ts
38
38
  import { execFileSync as execFileSync2 } from "node:child_process";
39
39
  import { createHash } from "node:crypto";
40
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
40
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
41
41
  import { userInfo } from "node:os";
42
42
  import path4 from "node:path";
43
43
 
@@ -395,6 +395,41 @@ function readJson(file) {
395
395
  const f = readJsonFile(file);
396
396
  return f.ok ? f.value : {};
397
397
  }
398
+ function refused(file, e, what, existed) {
399
+ const { code, syscall } = e;
400
+ const why = `can't ${what} it (${code ?? String(e)})`;
401
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
402
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
403
+ return {
404
+ file,
405
+ changed: true,
406
+ skipped: true,
407
+ ...existed ? {} : { created: true },
408
+ note: `${why} part way; check the file before the harness reads it`
409
+ };
410
+ }
411
+ function writeBack(file, text, existed = true) {
412
+ try {
413
+ mkdirSync(path3.dirname(file), { recursive: true });
414
+ writeFileSync(file, text);
415
+ return null;
416
+ } catch (e) {
417
+ return refused(file, e, "write", existed);
418
+ }
419
+ }
420
+ function removeEmptied(file, empty) {
421
+ try {
422
+ unlinkSync(file);
423
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
424
+ } catch (e) {
425
+ const bad = refused(file, e, "remove", true);
426
+ return writeBack(file, empty) ?? {
427
+ file,
428
+ changed: true,
429
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
430
+ };
431
+ }
432
+ }
398
433
  function entryFor(event, harness, cmd, o) {
399
434
  const matcher = o.matcherFor(event);
400
435
  return {
@@ -436,8 +471,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
436
471
  }
437
472
  if (changed) {
438
473
  settings2.hooks = hooks;
439
- mkdirSync(path3.dirname(file), { recursive: true });
440
- writeFileSync(file, formatJson(settings2, f));
474
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
475
+ if (bad) return bad;
441
476
  }
442
477
  return { file, changed, created: !f.existed && changed };
443
478
  }
@@ -460,12 +495,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
460
495
  }
461
496
  if (!changed) return { file, changed: false };
462
497
  if (!Object.keys(hooks).length) delete settings2.hooks;
463
- if (opts.created && !Object.keys(settings2).length) {
464
- unlinkSync(file);
465
- return { file, changed: true, note: "removed (sessionpipe created it)" };
466
- }
467
- writeFileSync(file, formatJson(settings2, f));
468
- return { file, changed: true };
498
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
499
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
469
500
  }
470
501
  function installedClaudeShaped(file, harness, events, cmd, o) {
471
502
  const f = readJsonFile(file);
@@ -544,7 +575,7 @@ var claudeCode = {
544
575
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
545
576
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
546
577
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
547
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
578
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
548
579
  return line ? [hooks, line] : [hooks];
549
580
  }),
550
581
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -552,7 +583,9 @@ var claudeCode = {
552
583
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
553
584
  created: opts.created?.includes(settingsIn(d))
554
585
  });
555
- return line && !hooks.changed ? line : hooks;
586
+ if (line?.changed && !line.skipped && hooks.skipped)
587
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
588
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
556
589
  }),
557
590
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
558
591
  file: settingsIn(d),
@@ -647,7 +680,8 @@ var claudeCode = {
647
680
  attention_id: call_id ?? permissionId(),
648
681
  kind: "permission",
649
682
  tool,
650
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
683
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
684
+ agent_id
651
685
  })
652
686
  );
653
687
  break;
@@ -656,7 +690,8 @@ var claudeCode = {
656
690
  ev("attention.needed", {
657
691
  attention_id: `notif-${Date.now()}`,
658
692
  kind: notifKind(s.notification_type),
659
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
693
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
694
+ agent_id
660
695
  })
661
696
  );
662
697
  break;
@@ -781,8 +816,11 @@ function statusLine(file, want, cmd) {
781
816
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
782
817
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
783
818
  else delete f.value.statusLine;
784
- writeFileSync2(file, formatJson(f.value, f));
785
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
819
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
820
+ file,
821
+ changed: true,
822
+ note: want ? "status line written (account limits)" : "status line removed"
823
+ };
786
824
  }
787
825
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
788
826
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1388,16 +1426,7 @@ function runHeadless(bin, args, o) {
1388
1426
  }
1389
1427
 
1390
1428
  // ../core/src/adapters/antigravity.ts
1391
- import {
1392
- existsSync as existsSync3,
1393
- mkdirSync as mkdirSync2,
1394
- readdirSync as readdirSync3,
1395
- readFileSync as readFileSync4,
1396
- realpathSync,
1397
- statSync as statSync4,
1398
- unlinkSync as unlinkSync2,
1399
- writeFileSync as writeFileSync3
1400
- } from "node:fs";
1429
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync4 } from "node:fs";
1401
1430
  import path6 from "node:path";
1402
1431
  var NAME2 = "antigravity";
1403
1432
  var HOOK_NAME = "sessionpipe";
@@ -1429,10 +1458,8 @@ var antigravity = {
1429
1458
  const e = entry(cmd);
1430
1459
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1431
1460
  hooks[HOOK_NAME] = e;
1432
- mkdirSync2(path6.dirname(file), { recursive: true });
1433
- writeFileSync3(file, formatJson(hooks, f));
1434
1461
  return [
1435
- {
1462
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
1436
1463
  file,
1437
1464
  changed: true,
1438
1465
  created: !f.existed,
@@ -1448,12 +1475,8 @@ var antigravity = {
1448
1475
  const hooks = f.value;
1449
1476
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1450
1477
  delete hooks[HOOK_NAME];
1451
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1452
- unlinkSync2(file);
1453
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1454
- }
1455
- writeFileSync3(file, formatJson(hooks, f));
1456
- return [{ file, changed: true }];
1478
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
1479
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
1457
1480
  },
1458
1481
  installed(cmd, env = process.env) {
1459
1482
  const file = hooksFile(env);
@@ -2186,7 +2209,7 @@ function parseResult(stdout) {
2186
2209
  var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
2187
2210
 
2188
2211
  // ../core/src/adapters/codex.ts
2189
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2212
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
2190
2213
  import path8 from "node:path";
2191
2214
 
2192
2215
  // ../core/src/adapters/codex-config.ts
@@ -2339,29 +2362,30 @@ var configFile2 = (env) => path8.join(codexHome(env), "config.toml");
2339
2362
  var sessionsDir = (env) => path8.join(codexHome(env), "sessions");
2340
2363
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2341
2364
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
2365
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
2342
2366
  function installNotify(cmd, env) {
2343
2367
  const file = configFile2(env);
2344
2368
  let text = "";
2369
+ let existed = true;
2345
2370
  try {
2346
2371
  text = readFileSync6(file, "utf8");
2347
- } catch {
2372
+ } catch (e) {
2373
+ existed = e.code !== "ENOENT";
2348
2374
  }
2349
2375
  const argv = cmd([NAME3, "notify"]).argv;
2350
2376
  const line = `notify = ${JSON.stringify(argv)}`;
2351
2377
  const n = readNotify(text);
2352
- const write = (next) => {
2353
- mkdirSync3(path8.dirname(file), { recursive: true });
2354
- writeFileSync4(file, next);
2378
+ const write = (next, note) => {
2379
+ const bad = writeBack(file, next, existed);
2380
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
2355
2381
  };
2356
2382
  if (n && !n.other) {
2357
2383
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
2358
2384
  return { file, changed: false, note: "notify current" };
2359
- write(withNotify(text, line));
2360
- return {
2361
- file,
2362
- changed: true,
2363
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
2364
- };
2385
+ return write(
2386
+ withNotify(text, line),
2387
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
2388
+ );
2365
2389
  }
2366
2390
  if (n?.other) {
2367
2391
  const prev = previousNotify(n.argv);
@@ -2370,20 +2394,20 @@ function installNotify(cmd, env) {
2370
2394
  if (JSON.stringify(prev) === JSON.stringify(argv))
2371
2395
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
2372
2396
  }
2373
- if (!prev || OURS2.test(JSON.stringify(prev))) {
2374
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
2375
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
2376
- }
2397
+ if (!prev || OURS2.test(JSON.stringify(prev)))
2398
+ return write(
2399
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
2400
+ "chained after Codex Computer Use (--previous-notify)"
2401
+ );
2377
2402
  }
2378
2403
  return {
2379
2404
  file,
2380
2405
  changed: false,
2381
2406
  skipped: true,
2382
- 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`
2407
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
2383
2408
  };
2384
2409
  }
2385
- write(withNotify(text, line));
2386
- return { file, changed: true, note: "notify set at the top level" };
2410
+ return write(withNotify(text, line), "notify set at the top level");
2387
2411
  }
2388
2412
  function uninstallNotify(env) {
2389
2413
  const file = configFile2(env);
@@ -2395,15 +2419,13 @@ function uninstallNotify(env) {
2395
2419
  }
2396
2420
  const n = readNotify(text);
2397
2421
  if (!n) return { file, changed: false };
2398
- if (!n.other) {
2399
- writeFileSync4(file, withoutNotify(text));
2400
- return { file, changed: true };
2401
- }
2422
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
2402
2423
  const prev = previousNotify(n.argv);
2403
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
2404
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
2405
- return { file, changed: true };
2406
- }
2424
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
2425
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
2426
+ file,
2427
+ changed: true
2428
+ };
2407
2429
  return { file, changed: false };
2408
2430
  }
2409
2431
  function findRollout(threadId, env = process.env) {
@@ -2435,8 +2457,12 @@ function rolloutMeta(file) {
2435
2457
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
2436
2458
  }
2437
2459
  function ofAnotherThread(file, id) {
2460
+ const other = threadOf(file);
2461
+ return !!other && other !== id.toLowerCase();
2462
+ }
2463
+ function threadOf(file) {
2438
2464
  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);
2439
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
2465
+ return m?.[1] ? m[1].toLowerCase() : null;
2440
2466
  }
2441
2467
  var codex = {
2442
2468
  name: NAME3,
@@ -2515,6 +2541,8 @@ var codex = {
2515
2541
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
2516
2542
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
2517
2543
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2544
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
2545
+ 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;
2518
2546
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
2519
2547
  const events = [];
2520
2548
  switch (event) {
@@ -2534,17 +2562,30 @@ var codex = {
2534
2562
  );
2535
2563
  break;
2536
2564
  case "PreToolUse":
2537
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2565
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
2538
2566
  break;
2539
2567
  case "PostToolUse":
2540
2568
  if (tool)
2541
2569
  events.push(
2542
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
2570
+ ev("tool.ended", {
2571
+ tool,
2572
+ call_id,
2573
+ turn_id,
2574
+ agent_id,
2575
+ ok: true,
2576
+ input: s.tool_input,
2577
+ output: s.tool_response
2578
+ })
2543
2579
  );
2544
2580
  break;
2545
2581
  case "PermissionRequest":
2546
2582
  events.push(
2547
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
2583
+ ev("attention.needed", {
2584
+ attention_id: call_id ?? `perm-${Date.now()}`,
2585
+ kind: "permission",
2586
+ tool,
2587
+ agent_id
2588
+ })
2548
2589
  );
2549
2590
  break;
2550
2591
  case "Stop":
@@ -3034,7 +3075,7 @@ var CHAIN_LIMIT = 4e3;
3034
3075
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
3035
3076
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
3036
3077
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
3037
- UNION ALL
3078
+ UNION
3038
3079
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
3039
3080
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
3040
3081
  )
@@ -3303,7 +3344,7 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
3303
3344
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
3304
3345
 
3305
3346
  // ../core/src/config.ts
3306
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
3347
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
3307
3348
  import path11 from "node:path";
3308
3349
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
3309
3350
  function readConfig(file = configFile()) {
@@ -3319,9 +3360,36 @@ function readConfig(file = configFile()) {
3319
3360
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
3320
3361
  }
3321
3362
  }
3363
+ function configProblem(file) {
3364
+ let text;
3365
+ try {
3366
+ text = readFileSync8(file, "utf8");
3367
+ } catch (e) {
3368
+ const code = e.code;
3369
+ return code === "ENOENT" ? void 0 : `could not be read (${code})`;
3370
+ }
3371
+ if (!text.trim()) return void 0;
3372
+ if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
3373
+ try {
3374
+ const j = JSON.parse(text);
3375
+ return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
3376
+ } catch (e) {
3377
+ const m = e.message;
3378
+ const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
3379
+ return `is not valid JSON${at ? ` at ${at}` : ""}`;
3380
+ }
3381
+ }
3382
+ function assertConfigWritable(file = configFile()) {
3383
+ const problem = configProblem(file);
3384
+ if (problem)
3385
+ throw new Error(
3386
+ `${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.`
3387
+ );
3388
+ }
3322
3389
  function writeConfig(c, file = configFile()) {
3323
- mkdirSync4(path11.dirname(file), { recursive: true, mode: 448 });
3324
- writeFileSync5(file, `${JSON.stringify(c, null, 2)}
3390
+ assertConfigWritable(file);
3391
+ mkdirSync2(path11.dirname(file), { recursive: true, mode: 448 });
3392
+ writeFileSync2(file, `${JSON.stringify(c, null, 2)}
3325
3393
  `, { mode: 384 });
3326
3394
  if (process.platform !== "win32" && existsSync8(file)) {
3327
3395
  try {
@@ -3385,19 +3453,20 @@ function makeEvent(a, ctx) {
3385
3453
  }
3386
3454
 
3387
3455
  // ../core/src/outbox.ts
3456
+ import { randomBytes as randomBytes2 } from "node:crypto";
3388
3457
  import {
3389
3458
  appendFileSync,
3390
3459
  closeSync as closeSync3,
3391
3460
  existsSync as existsSync9,
3392
- mkdirSync as mkdirSync5,
3461
+ mkdirSync as mkdirSync3,
3393
3462
  openSync as openSync3,
3394
3463
  readdirSync as readdirSync6,
3395
3464
  readFileSync as readFileSync9,
3396
3465
  readSync as readSync3,
3397
3466
  renameSync,
3398
3467
  statSync as statSync6,
3399
- unlinkSync as unlinkSync3,
3400
- writeFileSync as writeFileSync6
3468
+ unlinkSync as unlinkSync2,
3469
+ writeFileSync as writeFileSync3
3401
3470
  } from "node:fs";
3402
3471
  import path12 from "node:path";
3403
3472
  var READ_CHUNK = 1024 * 1024;
@@ -3427,8 +3496,8 @@ var Outbox = class {
3427
3496
  } catch {
3428
3497
  }
3429
3498
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
3430
- mkdirSync5(path12.dirname(f), { recursive: true, mode: 448 });
3431
- writeFileSync6(f, String(n + 1), { mode: 384 });
3499
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3500
+ writeFileSync3(f, String(n + 1), { mode: 384 });
3432
3501
  return n;
3433
3502
  }
3434
3503
  lastSeqInFile(ref) {
@@ -3446,7 +3515,7 @@ var Outbox = class {
3446
3515
  }
3447
3516
  append(ref, events) {
3448
3517
  if (!events.length) return;
3449
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
3518
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
3450
3519
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
3451
3520
  `, { mode: 384 });
3452
3521
  }
@@ -3516,34 +3585,106 @@ var Outbox = class {
3516
3585
  closeSync3(fd);
3517
3586
  }
3518
3587
  }
3519
- /** Files older than `days` are pruned; the cursor files with them. */
3588
+ /** Files older than `days` are pruned, and each sink's cursor into them. The seq
3589
+ * counter stays: a session resumed after the window counts on from where it was,
3590
+ * so a receiver's (harness, session, seq) dedup doesn't take its new events for
3591
+ * ones it already has, and the sinks read the new file from its start.
3592
+ * It all happens under the drain's `flush` lock, since a drain holds the whole
3593
+ * cursor map and writes it back: a file and its cursor keys go together, so a
3594
+ * session resumed in between can't keep a key past its new file's end. While a
3595
+ * drain runs, nothing is pruned (0); the next prune does it. It never throws: it
3596
+ * is a cleanup, and a hook's drain comes after it. */
3520
3597
  prune(days, now = Date.now()) {
3598
+ let release = null;
3599
+ try {
3600
+ release = takeNamedLock(this.root, "flush");
3601
+ } catch {
3602
+ }
3603
+ if (!release) return 0;
3521
3604
  let n = 0;
3522
- const cutoff = now - days * 864e5;
3523
- for (const ref of this.sessions()) {
3524
- const f = this.file(ref);
3525
- try {
3526
- if (statSync6(f).mtimeMs < cutoff) {
3527
- unlinkSync3(f);
3528
- try {
3529
- unlinkSync3(this.seqFile(ref));
3530
- } catch {
3605
+ try {
3606
+ const cutoff = now - days * 864e5;
3607
+ const aged = this.sessions().filter((ref) => {
3608
+ try {
3609
+ return statSync6(this.file(ref)).mtimeMs < cutoff;
3610
+ } catch {
3611
+ return false;
3612
+ }
3613
+ });
3614
+ this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
3615
+ for (const ref of aged) {
3616
+ const f = this.file(ref);
3617
+ try {
3618
+ if (statSync6(f).mtimeMs < cutoff) {
3619
+ unlinkSync2(f);
3620
+ n++;
3531
3621
  }
3532
- n++;
3622
+ } catch {
3533
3623
  }
3534
- } catch {
3535
3624
  }
3625
+ } catch {
3626
+ } finally {
3627
+ release();
3536
3628
  }
3537
3629
  return n;
3538
3630
  }
3631
+ /** A sink's cursor into a session file about to be pruned (`gone`), or one already
3632
+ * missing (an earlier prune, an older release, by hand) is removed. The caller
3633
+ * holds the `flush` lock. */
3634
+ dropStaleCursors(gone) {
3635
+ let names = [];
3636
+ try {
3637
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3638
+ } catch {
3639
+ }
3640
+ const stale2 = (c) => Object.keys(c).filter((k) => {
3641
+ if (gone.has(k)) return true;
3642
+ const i = k.indexOf("/");
3643
+ return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
3644
+ });
3645
+ for (const n of names) {
3646
+ const s = new Cursors(this.root, n.slice(0, -5));
3647
+ const c = s.read();
3648
+ const keys = stale2(c);
3649
+ if (!keys.length) continue;
3650
+ for (const k of keys) delete c[k];
3651
+ s.write(c);
3652
+ }
3653
+ }
3539
3654
  delete(ref) {
3655
+ this.dropCursors(ref);
3540
3656
  for (const f of [this.file(ref), this.seqFile(ref)]) {
3541
3657
  try {
3542
- unlinkSync3(f);
3658
+ unlinkSync2(f);
3543
3659
  } catch {
3544
3660
  }
3545
3661
  }
3546
3662
  }
3663
+ /** The key every sink's cursor holds for a session is removed with the session's
3664
+ * file: the key alone pointed into the old file, and a session that went on (still
3665
+ * running, or resumed later) had its new file's first events unsent until it grew
3666
+ * past that offset. Every file under <state>/cursors is taken (a sink since taken
3667
+ * out of the config still has one; one that doesn't parse reads as empty and is
3668
+ * left), by the key a drain writes (the file's own name, `safeId`) and by the ref
3669
+ * as given. The caller holds the drain's `flush` lock: a drain holds the whole
3670
+ * cursor map and writes it back, so a key dropped under it would come back. */
3671
+ dropCursors(ref) {
3672
+ const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
3673
+ let names = [];
3674
+ try {
3675
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3676
+ } catch {
3677
+ return;
3678
+ }
3679
+ for (const n of names) {
3680
+ const s = new Cursors(this.root, n.slice(0, -5));
3681
+ const c = s.read();
3682
+ const had = Object.keys(c).filter((k) => keys.has(k));
3683
+ if (!had.length) continue;
3684
+ for (const k of had) delete c[k];
3685
+ s.write(c);
3686
+ }
3687
+ }
3547
3688
  };
3548
3689
  var Cursors = class {
3549
3690
  constructor(root, sink) {
@@ -3555,18 +3696,21 @@ var Cursors = class {
3555
3696
  file() {
3556
3697
  return path12.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3557
3698
  }
3699
+ /** The map; empty when the file is missing, cut off mid-write, or holds anything
3700
+ * but an object (`null`, a list), so a drain never indexes into those. */
3558
3701
  read() {
3559
3702
  try {
3560
- return JSON.parse(readFileSync9(this.file(), "utf8"));
3703
+ const c = JSON.parse(readFileSync9(this.file(), "utf8"));
3704
+ return c && typeof c === "object" && !Array.isArray(c) ? c : {};
3561
3705
  } catch {
3562
3706
  return {};
3563
3707
  }
3564
3708
  }
3565
3709
  write(c) {
3566
3710
  const f = this.file();
3567
- mkdirSync5(path12.dirname(f), { recursive: true, mode: 448 });
3711
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3568
3712
  const tmp = `${f}.${process.pid}.tmp`;
3569
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
3713
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
3570
3714
  renameSync(tmp, f);
3571
3715
  }
3572
3716
  key(ref) {
@@ -3578,21 +3722,43 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
3578
3722
  }
3579
3723
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
3580
3724
  const dir = path12.join(root, "locks");
3581
- mkdirSync5(dir, { recursive: true, mode: 448 });
3725
+ mkdirSync3(dir, { recursive: true, mode: 448 });
3582
3726
  const f = path12.join(dir, `${name}.lock`);
3727
+ const nonce = randomBytes2(8).toString("hex");
3728
+ const mine = `${process.pid} ${nonce}`;
3729
+ let body = null;
3583
3730
  try {
3584
- if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync9(f, "utf8")))) unlinkSync3(f);
3585
- else return null;
3731
+ body = readFileSync9(f, "utf8");
3732
+ const stale2 = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
3733
+ if (!stale2) return null;
3586
3734
  } catch {
3735
+ body = null;
3736
+ }
3737
+ if (body !== null) {
3738
+ const aside = `${f}.${process.pid}.${nonce}.stale`;
3739
+ try {
3740
+ renameSync(f, aside);
3741
+ } catch {
3742
+ return null;
3743
+ }
3744
+ const moved = readFileSync9(aside, "utf8");
3745
+ unlinkSync2(aside);
3746
+ if (moved !== body) {
3747
+ try {
3748
+ writeFileSync3(f, moved, { flag: "wx" });
3749
+ } catch {
3750
+ }
3751
+ return null;
3752
+ }
3587
3753
  }
3588
3754
  try {
3589
- writeFileSync6(f, String(process.pid), { flag: "wx" });
3755
+ writeFileSync3(f, mine, { flag: "wx" });
3590
3756
  } catch {
3591
3757
  return null;
3592
3758
  }
3593
3759
  return () => {
3594
3760
  try {
3595
- unlinkSync3(f);
3761
+ if (readFileSync9(f, "utf8") === mine) unlinkSync2(f);
3596
3762
  } catch {
3597
3763
  }
3598
3764
  };
@@ -3611,7 +3777,7 @@ function processAlive(pid) {
3611
3777
  import { createHash as createHash3 } from "node:crypto";
3612
3778
  var RULESET_PII = "pii@1";
3613
3779
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
3614
- var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
3780
+ 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;
3615
3781
  function looksPhone(s) {
3616
3782
  const digits = s.replace(/\D/g, "");
3617
3783
  if (digits.length < 7 || digits.length > 15) return false;
@@ -3622,7 +3788,7 @@ function looksPhone(s) {
3622
3788
  function reducePii(text, home) {
3623
3789
  if (!text) return text;
3624
3790
  let out = text.replace(EMAIL, "[email]");
3625
- out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
3791
+ out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
3626
3792
  if (home) {
3627
3793
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3628
3794
  out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -3836,7 +4002,7 @@ var DATA_ALLOW = {
3836
4002
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
3837
4003
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
3838
4004
  "files.changed": { 1: ["paths"] },
3839
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
4005
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
3840
4006
  "attention.cleared": { 0: ["attention_id", "how"] },
3841
4007
  "subagent.started": { 1: ["agent_id", "agent_type"] },
3842
4008
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
@@ -3927,7 +4093,7 @@ function shortId(harness, id) {
3927
4093
  }
3928
4094
 
3929
4095
  // ../core/src/sinks/file.ts
3930
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
4096
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
3931
4097
  import path13 from "node:path";
3932
4098
  var FileSink = class {
3933
4099
  constructor(name, file, tier, pii) {
@@ -3942,7 +4108,7 @@ var FileSink = class {
3942
4108
  pii;
3943
4109
  async send(events) {
3944
4110
  try {
3945
- mkdirSync6(path13.dirname(this.file), { recursive: true });
4111
+ mkdirSync4(path13.dirname(this.file), { recursive: true });
3946
4112
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3947
4113
  `);
3948
4114
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3979,24 +4145,24 @@ var StdoutSink = class {
3979
4145
  import {
3980
4146
  appendFileSync as appendFileSync3,
3981
4147
  existsSync as existsSync10,
3982
- mkdirSync as mkdirSync8,
4148
+ mkdirSync as mkdirSync7,
3983
4149
  readdirSync as readdirSync7,
3984
- readFileSync as readFileSync11,
3985
- renameSync as renameSync3,
4150
+ readFileSync as readFileSync12,
4151
+ renameSync as renameSync4,
3986
4152
  statSync as statSync7,
3987
- unlinkSync as unlinkSync4,
3988
- writeFileSync as writeFileSync8
4153
+ unlinkSync as unlinkSync3,
4154
+ writeFileSync as writeFileSync6
3989
4155
  } from "node:fs";
3990
4156
  import os4 from "node:os";
3991
- import path16 from "node:path";
4157
+ import path17 from "node:path";
3992
4158
 
3993
4159
  // src/control/aliases.ts
3994
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
4160
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
3995
4161
  import path14 from "node:path";
3996
4162
 
3997
4163
  // src/secrets.ts
3998
4164
  import { spawnSync } from "node:child_process";
3999
- import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
4165
+ import { createHash as createHash5, randomBytes as randomBytes3 } from "node:crypto";
4000
4166
  var SERVICE = "sessionpipe";
4001
4167
  var defaultRunner = (cmd, args, input) => {
4002
4168
  const r = spawnSync(cmd, args, {
@@ -4020,9 +4186,9 @@ function secretGet(store, account, run = defaultRunner) {
4020
4186
  function sinkWithSecrets(s, run = defaultRunner) {
4021
4187
  if (!s.token_in) return s;
4022
4188
  const token = secretGet(s.token_in, sinkAccount(s.name), run);
4023
- if (!token) return null;
4024
4189
  const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
4025
- return { ...s, token, ...secret ? { secret } : {} };
4190
+ if (!token && !secret) return null;
4191
+ return { ...s, ...token ? { token } : {}, ...secret ? { secret } : {} };
4026
4192
  }
4027
4193
 
4028
4194
  // src/control/store.ts
@@ -4097,6 +4263,76 @@ function ask(sock, req, timeoutMs) {
4097
4263
  });
4098
4264
  }
4099
4265
 
4266
+ // src/fold.ts
4267
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
4268
+ import path16 from "node:path";
4269
+ var TOOLS_MODES = ["all", "main", "beat"];
4270
+ var TOOL_FOLD_MS = 6e4;
4271
+ var KEEP_MS = 24 * 60 * 6e4;
4272
+ function isToolsMode(v) {
4273
+ return typeof v === "string" && TOOLS_MODES.includes(v);
4274
+ }
4275
+ function toolsMode(s) {
4276
+ if (isToolsMode(s.tools)) return s.tools;
4277
+ if (isToolsMode(s.receiver_tools)) return s.receiver_tools;
4278
+ return "all";
4279
+ }
4280
+ var agentOf = (e) => typeof e.data.agent_id === "string" ? e.data.agent_id : "";
4281
+ var thins = (mode, agent) => mode === "beat" || mode === "main" && agent !== "";
4282
+ var askKey = (key, agent) => `${key}\0ask\0${agent}`;
4283
+ function sendTool(e, mode, clock, key) {
4284
+ if (mode === "all") return true;
4285
+ const agent = agentOf(e);
4286
+ const t = Date.parse(e.time);
4287
+ if (e.type === "attention.needed") {
4288
+ if (thins(mode, agent)) clock[askKey(key, agent)] = Number.isFinite(t) ? t : 0;
4289
+ return true;
4290
+ }
4291
+ if (e.type !== "tool.started" && e.type !== "tool.ended" || !thins(mode, agent)) return true;
4292
+ if (e.type === "tool.started") return false;
4293
+ if (!Number.isFinite(t)) return true;
4294
+ const ask2 = askKey(key, agent);
4295
+ const asked = clock[ask2];
4296
+ if (asked !== void 0 && t >= asked) {
4297
+ delete clock[ask2];
4298
+ clock[key] = t;
4299
+ return true;
4300
+ }
4301
+ const last = clock[key];
4302
+ if (last !== void 0 && t > last - TOOL_FOLD_MS && t < last + TOOL_FOLD_MS) return false;
4303
+ clock[key] = t;
4304
+ return true;
4305
+ }
4306
+ var ToolClock = class {
4307
+ constructor(root, sink) {
4308
+ this.root = root;
4309
+ this.sink = sink;
4310
+ }
4311
+ root;
4312
+ sink;
4313
+ file() {
4314
+ return path16.join(this.root, "folds", `${safeId(this.sink)}.json`);
4315
+ }
4316
+ read() {
4317
+ try {
4318
+ const j = JSON.parse(readFileSync11(this.file(), "utf8"));
4319
+ return j && typeof j === "object" && !Array.isArray(j) ? j : {};
4320
+ } catch {
4321
+ return {};
4322
+ }
4323
+ }
4324
+ write(c) {
4325
+ const newest = Math.max(0, ...Object.values(c).filter(Number.isFinite));
4326
+ const kept = {};
4327
+ for (const [k, v] of Object.entries(c)) if (Number.isFinite(v) && v >= newest - KEEP_MS) kept[k] = v;
4328
+ const f = this.file();
4329
+ mkdirSync6(path16.dirname(f), { recursive: true, mode: 448 });
4330
+ const tmp = `${f}.${process.pid}.tmp`;
4331
+ writeFileSync5(tmp, JSON.stringify(kept), { mode: 384 });
4332
+ renameSync3(tmp, f);
4333
+ }
4334
+ };
4335
+
4100
4336
  // src/http-sink.ts
4101
4337
  import { createHash as createHash6, createHmac } from "node:crypto";
4102
4338
  var TIMEOUT_MS = 15e3;
@@ -4217,20 +4453,20 @@ async function tellDaemon(state, job, session, cwd, transcript) {
4217
4453
  var MAX_TRIES = 10;
4218
4454
  function enqueueJob(state, job) {
4219
4455
  const jobs = jobsDir(state);
4220
- mkdirSync8(jobs, { recursive: true, mode: 448 });
4221
- const f = path16.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4222
- writeFileSync8(f, JSON.stringify(job), { mode: 384 });
4456
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
4457
+ const f = path17.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4458
+ writeFileSync6(f, JSON.stringify(job), { mode: 384 });
4223
4459
  return f;
4224
4460
  }
4225
4461
  function claimJob(file) {
4226
4462
  const claimed = `${file}.${process.pid}.running`;
4227
4463
  try {
4228
- renameSync3(file, claimed);
4464
+ renameSync4(file, claimed);
4229
4465
  } catch {
4230
4466
  return null;
4231
4467
  }
4232
4468
  try {
4233
- return { job: JSON.parse(readFileSync11(claimed, "utf8")), file: claimed };
4469
+ return { job: JSON.parse(readFileSync12(claimed, "utf8")), file: claimed };
4234
4470
  } catch {
4235
4471
  releaseJob(claimed);
4236
4472
  return null;
@@ -4239,11 +4475,11 @@ function claimJob(file) {
4239
4475
  function releaseJob(claimed) {
4240
4476
  if (!claimed) return;
4241
4477
  try {
4242
- unlinkSync4(claimed);
4478
+ unlinkSync3(claimed);
4243
4479
  } catch {
4244
4480
  }
4245
4481
  }
4246
- async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4) {
4482
+ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4, o = {}) {
4247
4483
  const jobs = jobsDir(state);
4248
4484
  let names = [];
4249
4485
  try {
@@ -4255,17 +4491,18 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
4255
4491
  for (const n of names) {
4256
4492
  const m = /^(.*\.json)\.(\d+)\.running$/.exec(n);
4257
4493
  if (!m) continue;
4258
- const f = path16.join(jobs, n);
4494
+ const f = path17.join(jobs, n);
4259
4495
  try {
4260
4496
  if (processAlive(Number(m[2])) && Date.now() - statSync7(f).mtimeMs < abandonedMs) continue;
4261
- renameSync3(f, path16.join(jobs, m[1]));
4497
+ renameSync4(f, path17.join(jobs, m[1]));
4262
4498
  queued.push(m[1]);
4263
4499
  } catch {
4264
4500
  }
4265
4501
  }
4266
4502
  let ran = 0;
4267
4503
  for (const n of queued.sort().slice(0, limit)) {
4268
- const f = path16.join(jobs, n);
4504
+ if (o.deadline !== void 0 && Date.now() + SWEEP_LOCK_WAIT_MS + PASS_BUDGET_MS > o.deadline) break;
4505
+ const f = path17.join(jobs, n);
4269
4506
  try {
4270
4507
  if (Date.now() - statSync7(f).mtimeMs < olderThanMs) continue;
4271
4508
  } catch {
@@ -4274,21 +4511,23 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
4274
4511
  const c = claimJob(f);
4275
4512
  if (!c) continue;
4276
4513
  try {
4277
- await runJob(c.job, { state, claimed: c.file });
4514
+ const r = await runJob(c.job, { state, claimed: c.file, drain: false, lockWaitMs: SWEEP_LOCK_WAIT_MS });
4515
+ if (r.events.length) markMore(state);
4278
4516
  } catch {
4279
4517
  } finally {
4280
4518
  releaseJob(c.file);
4281
4519
  }
4282
4520
  ran++;
4283
4521
  }
4522
+ if (ran) await drain(state, readConfig().sinks, new Outbox(state), void 0, o);
4284
4523
  return ran;
4285
4524
  }
4286
4525
  function log(state, line) {
4287
4526
  try {
4288
- mkdirSync8(state, { recursive: true, mode: 448 });
4289
- const f = path16.join(state, "log");
4527
+ mkdirSync7(state, { recursive: true, mode: 448 });
4528
+ const f = path17.join(state, "log");
4290
4529
  try {
4291
- if (statSync7(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
4530
+ if (statSync7(f).size > 1024 * 1024) renameSync4(f, `${f}.1`);
4292
4531
  } catch {
4293
4532
  }
4294
4533
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -4297,11 +4536,11 @@ function log(state, line) {
4297
4536
  }
4298
4537
  }
4299
4538
  function factsState(state) {
4300
- const file = (h, s) => path16.join(state, "facts", safeId(h), `${safeId(s)}.json`);
4539
+ const file = (h, s) => path17.join(state, "facts", safeId(h), `${safeId(s)}.json`);
4301
4540
  return {
4302
4541
  get(h, s) {
4303
4542
  try {
4304
- return JSON.parse(readFileSync11(file(h, s), "utf8"));
4543
+ return JSON.parse(readFileSync12(file(h, s), "utf8"));
4305
4544
  } catch {
4306
4545
  return {};
4307
4546
  }
@@ -4310,8 +4549,8 @@ function factsState(state) {
4310
4549
  const cur = this.get(h, s);
4311
4550
  const next = { ...cur, ...patch };
4312
4551
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
4313
- mkdirSync8(path16.dirname(file(h, s)), { recursive: true, mode: 448 });
4314
- writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
4552
+ mkdirSync7(path17.dirname(file(h, s)), { recursive: true, mode: 448 });
4553
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
4315
4554
  }
4316
4555
  };
4317
4556
  }
@@ -4324,7 +4563,7 @@ function buildSinks(sinks, o = {}) {
4324
4563
  o.locked?.(raw.name);
4325
4564
  continue;
4326
4565
  }
4327
- const tier = Math.min(s.tier, s.max_tier ?? 3);
4566
+ const tier = Math.trunc(Math.min(Number(s.tier) || 0, s.max_tier ?? 3));
4328
4567
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
4329
4568
  else if (s.url.startsWith("file:"))
4330
4569
  out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os4.homedir()), tier, !!s.pii));
@@ -4386,7 +4625,7 @@ async function runJob(job, opts = {}) {
4386
4625
  );
4387
4626
  return { events: [], delivered: {} };
4388
4627
  }
4389
- const id = receiverIdOf(readAliases(aliasFile(path16.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
4628
+ const id = receiverIdOf(readAliases(aliasFile(path17.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
4390
4629
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
4391
4630
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
4392
4631
  const outbox = new Outbox(state);
@@ -4423,7 +4662,7 @@ async function runJob(job, opts = {}) {
4423
4662
  learned = rest;
4424
4663
  if (harness_version) facts.save(job.harness, id, { harness_version });
4425
4664
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
4426
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path16.isAbsolute(r.session.cwd))
4665
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path17.isAbsolute(r.session.cwd))
4427
4666
  facts.save(job.harness, id, { cwd: r.session.cwd });
4428
4667
  }
4429
4668
  const known = mem.session ?? {};
@@ -4541,29 +4780,84 @@ async function runJob(job, opts = {}) {
4541
4780
  } finally {
4542
4781
  release();
4543
4782
  }
4544
- const delivered = await drain(state, cfg.sinks, outbox, ref) ?? {};
4545
4783
  if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
4784
+ const delivered = opts.drain === false ? {} : await drain(state, cfg.sinks, outbox, ref, {
4785
+ ...opts.clock ? { clock: opts.clock } : {},
4786
+ ...opts.deadline !== void 0 ? { deadline: opts.deadline } : {},
4787
+ ...opts.handoff ? { handoff: opts.handoff } : {}
4788
+ }) ?? {};
4546
4789
  return { events, delivered };
4547
4790
  }
4548
- async function drain(state, sinkConfigs, outbox, first) {
4549
- const more = path16.join(state, "locks", "flush.more");
4791
+ var REAL_CLOCK = { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
4792
+ var PACE_MS = 1e3;
4793
+ var sentFile = (state) => path17.join(state, "locks", "flush.sent");
4794
+ function lastSent(state) {
4795
+ try {
4796
+ return Number(readFileSync12(sentFile(state), "utf8"));
4797
+ } catch {
4798
+ return 0;
4799
+ }
4800
+ }
4801
+ function noteSent(state, at) {
4802
+ try {
4803
+ mkdirSync7(path17.dirname(sentFile(state)), { recursive: true, mode: 448 });
4804
+ writeFileSync6(sentFile(state), String(at), { mode: 384 });
4805
+ } catch {
4806
+ }
4807
+ }
4808
+ function waitingForReceiver(state, sinkConfigs, outbox, now) {
4809
+ const receivers = sinkConfigs.filter(
4810
+ (s) => !s.paused && /^https?:\/\//.test(s.url) && !((readBackoff(state, s.name)?.until ?? 0) > now)
4811
+ );
4812
+ if (!receivers.length) return false;
4813
+ const refs = outbox.sessions();
4814
+ for (const s of receivers) {
4815
+ const cursors = new Cursors(state, s.name);
4816
+ const c = cursors.read();
4817
+ for (const ref of refs) {
4818
+ try {
4819
+ if (statSync7(outbox.file(ref)).size > (c[cursors.key(ref)] ?? 0)) return true;
4820
+ } catch {
4821
+ }
4822
+ }
4823
+ }
4824
+ return false;
4825
+ }
4826
+ var PASS_BUDGET_MS = 3e3;
4827
+ var SWEEP_LOCK_WAIT_MS = 1e3;
4828
+ var moreFile = (state) => path17.join(state, "locks", "flush.more");
4829
+ function markMore(state) {
4830
+ try {
4831
+ mkdirSync7(path17.dirname(moreFile(state)), { recursive: true, mode: 448 });
4832
+ writeFileSync6(moreFile(state), "", { mode: 384 });
4833
+ } catch {
4834
+ }
4835
+ }
4836
+ async function drain(state, sinkConfigs, outbox, first, o = {}) {
4837
+ const clock = o.clock ?? REAL_CLOCK;
4838
+ const more = moreFile(state);
4550
4839
  let release = takeNamedLock(state, "flush");
4551
4840
  if (!release) {
4552
- try {
4553
- writeFileSync8(more, "", { mode: 384 });
4554
- } catch {
4555
- }
4841
+ markMore(state);
4556
4842
  release = takeNamedLock(state, "flush");
4557
4843
  if (!release) return null;
4558
4844
  }
4559
4845
  const delivered = {};
4560
4846
  for (; ; ) {
4561
- try {
4562
- unlinkSync4(more);
4563
- } catch {
4847
+ if (o.deadline !== void 0 && clock.now() + PASS_BUDGET_MS > o.deadline) {
4848
+ markMore(state);
4849
+ release();
4850
+ o.handoff?.();
4851
+ return delivered;
4564
4852
  }
4565
4853
  try {
4566
- for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first)))
4854
+ const wait = Math.min(PACE_MS, lastSent(state) + PACE_MS - clock.now());
4855
+ if (wait > 0 && waitingForReceiver(state, sinkConfigs, outbox, clock.now())) await clock.sleep(wait);
4856
+ try {
4857
+ unlinkSync3(more);
4858
+ } catch {
4859
+ }
4860
+ for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first, { clock })))
4567
4861
  delivered[k] = (delivered[k] ?? 0) + v;
4568
4862
  } finally {
4569
4863
  release();
@@ -4574,10 +4868,10 @@ async function drain(state, sinkConfigs, outbox, first) {
4574
4868
  }
4575
4869
  }
4576
4870
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
4577
- var backoffFile = (state, sink) => path16.join(state, "backoff", `${safeId(sink)}.json`);
4871
+ var backoffFile = (state, sink) => path17.join(state, "backoff", `${safeId(sink)}.json`);
4578
4872
  function readBackoff(state, sink) {
4579
4873
  try {
4580
- const b = JSON.parse(readFileSync11(backoffFile(state, sink), "utf8"));
4874
+ const b = JSON.parse(readFileSync12(backoffFile(state, sink), "utf8"));
4581
4875
  return typeof b.until === "number" ? b : null;
4582
4876
  } catch {
4583
4877
  return null;
@@ -4590,15 +4884,15 @@ function noteFailure(state, sink, res, prev) {
4590
4884
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
4591
4885
  const f = backoffFile(state, sink);
4592
4886
  try {
4593
- mkdirSync8(path16.dirname(f), { recursive: true, mode: 448 });
4594
- writeFileSync8(f, JSON.stringify(b), { mode: 384 });
4887
+ mkdirSync7(path17.dirname(f), { recursive: true, mode: 448 });
4888
+ writeFileSync6(f, JSON.stringify(b), { mode: 384 });
4595
4889
  } catch {
4596
4890
  }
4597
4891
  return b;
4598
4892
  }
4599
4893
  function clearBackoff(state, sink) {
4600
4894
  try {
4601
- unlinkSync4(backoffFile(state, sink));
4895
+ unlinkSync3(backoffFile(state, sink));
4602
4896
  } catch {
4603
4897
  }
4604
4898
  }
@@ -4608,14 +4902,31 @@ var BATCH = 50;
4608
4902
  var MAX_PER_RUN = 500;
4609
4903
  async function flush(state, sinkConfigs, outbox, first, o = {}) {
4610
4904
  const delivered = {};
4905
+ const clock = o.clock ?? REAL_CLOCK;
4611
4906
  const sinks = buildSinks(sinkConfigs, {
4612
4907
  locked: (name) => log(state, `sink ${name}: its token is in a keychain this process can't read; left for later`)
4613
4908
  });
4909
+ const modes = new Map(sinkConfigs.map((s) => [s.name, toolsMode(s)]));
4614
4910
  const refs = outbox.sessions();
4615
4911
  if (first) {
4616
4912
  const k = `${first.harness}/${safeId(first.session)}`;
4617
4913
  refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
4618
4914
  }
4915
+ const unreadable = /* @__PURE__ */ new Set();
4916
+ const readOrSkip = (ref, offset, room) => {
4917
+ try {
4918
+ return outbox.read(ref, offset, room);
4919
+ } catch (e) {
4920
+ const code = e.code;
4921
+ if (typeof code !== "string") throw e;
4922
+ const f = outbox.file(ref);
4923
+ if (!unreadable.has(f)) {
4924
+ unreadable.add(f);
4925
+ log(state, `drain: can't read ${f} (${code}); skipped, its cursors stay`);
4926
+ }
4927
+ return null;
4928
+ }
4929
+ };
4619
4930
  const drainSink = async (sink) => {
4620
4931
  let backoff = readBackoff(state, sink.name);
4621
4932
  if (backoff && !o.force && backoff.until > Date.now()) {
@@ -4624,11 +4935,16 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4624
4935
  }
4625
4936
  const cursors = new Cursors(state, sink.name);
4626
4937
  const c = cursors.read();
4938
+ const mode = modes.get(sink.name);
4939
+ const tools = new ToolClock(state, sink.name);
4940
+ let clockNow = mode === "all" ? {} : tools.read();
4941
+ let clockNext = { ...clockNow };
4627
4942
  let sent = 0;
4628
4943
  let stalled = false;
4629
4944
  let batch = [];
4630
4945
  let pending = {};
4631
4946
  const deliver = async (events) => {
4947
+ if (sink instanceof HttpSink) noteSent(state, clock.now());
4632
4948
  const res = await sink.send(events);
4633
4949
  if (res.ok || res.action !== "split") return res;
4634
4950
  if (events.length === 1) {
@@ -4636,10 +4952,20 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4636
4952
  log(state, `sink ${sink.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
4637
4953
  return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
4638
4954
  }
4955
+ const part = async (some) => {
4956
+ const r = await deliver(some);
4957
+ if (r.ok || r.action !== "drop") return r;
4958
+ log(
4959
+ state,
4960
+ `sink ${sink.name}: ${r.status ?? ""} ${r.error ?? ""} \u2192 drop, ${some.length} of ${events.length} events`
4961
+ );
4962
+ const rejected = some.map((e) => ({ id: e.id, reason: "dropped" }));
4963
+ return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected };
4964
+ };
4639
4965
  const half = Math.ceil(events.length / 2);
4640
- const a = await deliver(events.slice(0, half));
4966
+ const a = await part(events.slice(0, half));
4641
4967
  if (!a.ok) return a;
4642
- const b = await deliver(events.slice(half));
4968
+ const b = await part(events.slice(half));
4643
4969
  if (!b.ok) return b;
4644
4970
  return {
4645
4971
  ok: true,
@@ -4649,11 +4975,17 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4649
4975
  rejected: [...a.rejected ?? [], ...b.rejected ?? []]
4650
4976
  };
4651
4977
  };
4978
+ const commitClock = () => {
4979
+ if (mode === "all" || JSON.stringify(clockNext) === JSON.stringify(clockNow)) return;
4980
+ tools.write(clockNext);
4981
+ clockNow = { ...clockNext };
4982
+ };
4652
4983
  const send = async () => {
4653
4984
  if (!batch.length) {
4654
4985
  Object.assign(c, pending);
4655
4986
  pending = {};
4656
4987
  cursors.write(c);
4988
+ commitClock();
4657
4989
  return true;
4658
4990
  }
4659
4991
  const res = await deliver(batch);
@@ -4669,6 +5001,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4669
5001
  if (res.action !== "drop") {
4670
5002
  batch = [];
4671
5003
  pending = {};
5004
+ clockNext = { ...clockNow };
4672
5005
  return false;
4673
5006
  }
4674
5007
  } else if (backoff) {
@@ -4678,6 +5011,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4678
5011
  sent += batch.length;
4679
5012
  Object.assign(c, pending);
4680
5013
  cursors.write(c);
5014
+ commitClock();
4681
5015
  batch = [];
4682
5016
  pending = {};
4683
5017
  return true;
@@ -4687,11 +5021,15 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4687
5021
  let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
4688
5022
  for (; ; ) {
4689
5023
  const room = BATCH - batch.length;
4690
- const { events, offset: next } = outbox.read(ref, offset, room);
5024
+ const read = readOrSkip(ref, offset, room);
5025
+ if (!read) break;
5026
+ const { events, offset: next } = read;
4691
5027
  if (!events.length) break;
4692
5028
  for (const e of events) {
4693
5029
  const f = filterEvent(e, sink.tier, sink.pii, { home: os4.homedir() });
4694
- if (f) batch.push(f);
5030
+ if (!f) continue;
5031
+ if (sendTool(f, mode, clockNext, cursors.key(ref))) batch.push(f);
5032
+ else if (o.thinned) o.thinned[sink.name] = (o.thinned[sink.name] ?? 0) + 1;
4695
5033
  }
4696
5034
  offset = next;
4697
5035
  pending[cursors.key(ref)] = offset;
@@ -4723,31 +5061,105 @@ function applySinkVerdict(name, res) {
4723
5061
  } catch {
4724
5062
  }
4725
5063
  }
4726
- var jobsDir = (state) => path16.join(state, "jobs");
5064
+ var jobsDir = (state) => path17.join(state, "jobs");
5065
+
5066
+ // src/wellknown.ts
5067
+ var WELL_KNOWN_MAX_AGE_MS = 24 * 60 * 6e4;
5068
+ var wellKnownUrl = (sinkUrl) => `${sinkUrl.replace(/\/$/, "")}/.well-known/sessionpipe`;
5069
+ async function readWellKnown(sinkUrl, timeoutMs, fetchFn = fetch) {
5070
+ const r = await fetchFn(wellKnownUrl(sinkUrl), { signal: AbortSignal.timeout(timeoutMs) });
5071
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
5072
+ return await r.json();
5073
+ }
5074
+ function applyWellKnown(s, wk, tier, now = /* @__PURE__ */ new Date()) {
5075
+ const notes = [];
5076
+ if (typeof wk.max_tier === "number") {
5077
+ s.max_tier = wk.max_tier;
5078
+ if (wk.max_tier < tier)
5079
+ notes.push(`receiver's max_tier is ${wk.max_tier}: sending tier ${wk.max_tier}, not ${tier}`);
5080
+ }
5081
+ if (wk.endpoints) s.endpoints = wk.endpoints;
5082
+ const before = s.receiver_tools;
5083
+ if (isToolsMode(wk.tools)) s.receiver_tools = wk.tools;
5084
+ else delete s.receiver_tools;
5085
+ if (s.receiver_tools !== before && s.receiver_tools && s.receiver_tools !== "all" && !isToolsMode(s.tools))
5086
+ notes.push(`the receiver asks for tool events \`${s.receiver_tools}\` (--tools all keeps every one)`);
5087
+ s.well_known_at = now.toISOString();
5088
+ s.well_known_checked_at = s.well_known_at;
5089
+ return notes;
5090
+ }
5091
+ var stale = (s, now) => {
5092
+ const last = Date.parse(s.well_known_checked_at ?? s.well_known_at ?? "");
5093
+ return !Number.isFinite(last) || now - last >= WELL_KNOWN_MAX_AGE_MS;
5094
+ };
5095
+ async function refreshWellKnown(cfg, o = {}) {
5096
+ const now = o.now ?? Date.now;
5097
+ let changed = false;
5098
+ for (const s of cfg.sinks) {
5099
+ if (!/^https?:\/\//.test(s.url) || !stale(s, now())) continue;
5100
+ const at = new Date(now());
5101
+ try {
5102
+ applyWellKnown(s, await readWellKnown(s.url, o.timeoutMs ?? 3e3, o.fetchFn), s.tier, at);
5103
+ } catch {
5104
+ s.well_known_checked_at = at.toISOString();
5105
+ }
5106
+ changed = true;
5107
+ }
5108
+ return changed;
5109
+ }
5110
+ async function refreshSinks(o = {}) {
5111
+ try {
5112
+ const cfg = readConfig();
5113
+ if (await refreshWellKnown(cfg, o)) writeConfig(cfg);
5114
+ } catch {
5115
+ }
5116
+ }
4727
5117
 
4728
5118
  // src/worker.ts
5119
+ var WORKER_LIFE_MS = 15e3;
5120
+ var DRAIN_ARG = "--drain";
5121
+ function startWorker(script, arg) {
5122
+ const p = spawn2(process.execPath, [script, arg], { detached: true, stdio: "ignore", windowsHide: true });
5123
+ p.unref();
5124
+ }
4729
5125
  async function workerMain(argv) {
4730
5126
  const jobFile = argv[2];
4731
5127
  if (!jobFile) return;
4732
- const claimed = claimJob(jobFile);
4733
- if (!claimed) return;
4734
- const deadline = setTimeout(() => process.exit(0), 15e3);
5128
+ const script = argv[1];
5129
+ let handedOff = false;
5130
+ const life = {
5131
+ deadline: Date.now() + WORKER_LIFE_MS,
5132
+ handoff: () => {
5133
+ if (handedOff) return;
5134
+ handedOff = true;
5135
+ startWorker(script, DRAIN_ARG);
5136
+ }
5137
+ };
5138
+ const deadline = setTimeout(() => process.exit(0), WORKER_LIFE_MS);
4735
5139
  deadline.unref();
4736
- await runJob(claimed.job, { claimed: claimed.file }).then((r) => {
5140
+ await refreshSinks();
5141
+ if (jobFile === DRAIN_ARG) {
5142
+ await drain(stateDir(), readConfig().sinks, new Outbox(stateDir()), void 0, life).then(() => sweepJobs(stateDir(), void 0, void 0, void 0, life)).catch(() => {
5143
+ }).finally(() => clearTimeout(deadline));
5144
+ return;
5145
+ }
5146
+ const claimed = claimJob(jobFile);
5147
+ if (!claimed) {
5148
+ clearTimeout(deadline);
5149
+ return;
5150
+ }
5151
+ await runJob(claimed.job, { claimed: claimed.file, ...life }).then((r) => {
4737
5152
  if (!r.requeued || (claimed.job.tries ?? 0) + 1 >= MAX_TRIES) return;
4738
- const p = spawn2(process.execPath, [argv[1], r.requeued], {
4739
- detached: true,
4740
- stdio: "ignore",
4741
- windowsHide: true
4742
- });
4743
- p.unref();
5153
+ startWorker(script, r.requeued);
4744
5154
  }).catch(() => {
4745
- }).finally(() => releaseJob(claimed.file)).then(() => sweepJobs(stateDir())).catch(() => {
5155
+ }).finally(() => releaseJob(claimed.file)).then(() => sweepJobs(stateDir(), void 0, void 0, void 0, life)).catch(() => {
4746
5156
  }).finally(() => clearTimeout(deadline));
4747
5157
  }
4748
5158
  if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync2(process.argv[1])).href)
4749
5159
  workerMain(process.argv).finally(() => process.exit(0));
4750
5160
  export {
5161
+ DRAIN_ARG,
5162
+ WORKER_LIFE_MS,
4751
5163
  workerMain
4752
5164
  };
4753
5165
  //# sourceMappingURL=worker.js.map