sessionpipe 0.13.0 → 0.13.2

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/cli.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.2 · Apache-2.0 · https://sessionpipe.org
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target2, all) => {
5
5
  for (var name in all)
@@ -12,14 +12,14 @@ import {
12
12
  existsSync as existsSync21,
13
13
  mkdtempSync,
14
14
  readdirSync as readdirSync8,
15
- readFileSync as readFileSync23,
15
+ readFileSync as readFileSync24,
16
16
  realpathSync as realpathSync9,
17
17
  rmSync as rmSync3,
18
18
  statSync as statSync14,
19
- unlinkSync as unlinkSync9
19
+ unlinkSync as unlinkSync8
20
20
  } from "node:fs";
21
21
  import os15 from "node:os";
22
- import path38 from "node:path";
22
+ import path39 from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
 
25
25
  // ../core/src/adapters/claude-code-pool.ts
@@ -76,7 +76,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
76
76
  // ../core/src/adapters/claude-code.ts
77
77
  import { execFileSync as execFileSync2 } from "node:child_process";
78
78
  import { createHash } from "node:crypto";
79
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
79
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
80
80
  import { userInfo } from "node:os";
81
81
  import path4 from "node:path";
82
82
 
@@ -434,6 +434,41 @@ function readJson(file) {
434
434
  const f = readJsonFile(file);
435
435
  return f.ok ? f.value : {};
436
436
  }
437
+ function refused(file, e, what, existed) {
438
+ const { code, syscall } = e;
439
+ const why2 = `can't ${what} it (${code ?? String(e)})`;
440
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
441
+ return { file, changed: false, skipped: true, note: `${why2}; not touched` };
442
+ return {
443
+ file,
444
+ changed: true,
445
+ skipped: true,
446
+ ...existed ? {} : { created: true },
447
+ note: `${why2} part way; check the file before the harness reads it`
448
+ };
449
+ }
450
+ function writeBack(file, text, existed = true) {
451
+ try {
452
+ mkdirSync(path3.dirname(file), { recursive: true });
453
+ writeFileSync(file, text);
454
+ return null;
455
+ } catch (e) {
456
+ return refused(file, e, "write", existed);
457
+ }
458
+ }
459
+ function removeEmptied(file, empty) {
460
+ try {
461
+ unlinkSync(file);
462
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
463
+ } catch (e) {
464
+ const bad2 = refused(file, e, "remove", true);
465
+ return writeBack(file, empty) ?? {
466
+ file,
467
+ changed: true,
468
+ note: `emptied; ${bad2.note.replace(/; not touched$/, "")}, so the empty file stays`
469
+ };
470
+ }
471
+ }
437
472
  function entryFor(event, harness, cmd, o) {
438
473
  const matcher = o.matcherFor(event);
439
474
  return {
@@ -475,8 +510,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
475
510
  }
476
511
  if (changed) {
477
512
  settings2.hooks = hooks;
478
- mkdirSync(path3.dirname(file), { recursive: true });
479
- writeFileSync(file, formatJson(settings2, f));
513
+ const bad2 = writeBack(file, formatJson(settings2, f), f.existed);
514
+ if (bad2) return bad2;
480
515
  }
481
516
  return { file, changed, created: !f.existed && changed };
482
517
  }
@@ -499,12 +534,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
499
534
  }
500
535
  if (!changed) return { file, changed: false };
501
536
  if (!Object.keys(hooks).length) delete settings2.hooks;
502
- if (opts.created && !Object.keys(settings2).length) {
503
- unlinkSync(file);
504
- return { file, changed: true, note: "removed (sessionpipe created it)" };
505
- }
506
- writeFileSync(file, formatJson(settings2, f));
507
- return { file, changed: true };
537
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
538
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
508
539
  }
509
540
  function installedClaudeShaped(file, harness, events, cmd, o) {
510
541
  const f = readJsonFile(file);
@@ -583,7 +614,7 @@ var claudeCode = {
583
614
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
584
615
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
585
616
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
586
- const line2 = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
617
+ const line2 = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
587
618
  return line2 ? [hooks, line2] : [hooks];
588
619
  }),
589
620
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -591,7 +622,9 @@ var claudeCode = {
591
622
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
592
623
  created: opts.created?.includes(settingsIn(d))
593
624
  });
594
- return line2 && !hooks.changed ? line2 : hooks;
625
+ if (line2?.changed && !line2.skipped && hooks.skipped)
626
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
627
+ return line2 && !hooks.changed && !hooks.skipped ? line2 : hooks;
595
628
  }),
596
629
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
597
630
  file: settingsIn(d),
@@ -686,7 +719,8 @@ var claudeCode = {
686
719
  attention_id: call_id ?? permissionId(),
687
720
  kind: "permission",
688
721
  tool,
689
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
722
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
723
+ agent_id
690
724
  })
691
725
  );
692
726
  break;
@@ -695,7 +729,8 @@ var claudeCode = {
695
729
  ev("attention.needed", {
696
730
  attention_id: `notif-${Date.now()}`,
697
731
  kind: notifKind(s.notification_type),
698
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
732
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
733
+ agent_id
699
734
  })
700
735
  );
701
736
  break;
@@ -820,8 +855,11 @@ function statusLine(file, want, cmd) {
820
855
  if (want ? ours && cur?.command === command2 && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
821
856
  if (want) f.value.statusLine = { type: "command", command: command2, refreshInterval: LINE_REFRESH_S };
822
857
  else delete f.value.statusLine;
823
- writeFileSync2(file, formatJson(f.value, f));
824
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
858
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
859
+ file,
860
+ changed: true,
861
+ note: want ? "status line written (account limits)" : "status line removed"
862
+ };
825
863
  }
826
864
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
827
865
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -2183,18 +2221,18 @@ function startAcp(o) {
2183
2221
  );
2184
2222
  const stop = str(res?.stopReason);
2185
2223
  const words = t.text.trim() || `(${label} finished without a written reply.)`;
2186
- const refused = deniedNote(t.denied, t.count);
2187
- if (stop === "end_turn" || stop === "max_tokens") return { reply: words + refused };
2224
+ const refused2 = deniedNote(t.denied, t.count);
2225
+ if (stop === "end_turn" || stop === "max_tokens") return { reply: words + refused2 };
2188
2226
  if (stop === "refusal") return { reply: `${words}
2189
2227
 
2190
- (${label} refused the request.)${refused}` };
2228
+ (${label} refused the request.)${refused2}` };
2191
2229
  if (stop === "cancelled")
2192
2230
  return timedOut ? {
2193
2231
  reply: "",
2194
2232
  error: `${label} was still working after ${Math.round(limit / 6e4)} minutes, so it was stopped`
2195
2233
  } : { reply: `${words}
2196
2234
 
2197
- (The turn was cancelled.)${refused}` };
2235
+ (The turn was cancelled.)${refused2}` };
2198
2236
  return { reply: "", error: `${label} ended the turn with ${stop || "no stop reason"}` };
2199
2237
  } catch (e) {
2200
2238
  return { reply: "", error: e.message };
@@ -2255,16 +2293,7 @@ function startAcp(o) {
2255
2293
  }
2256
2294
 
2257
2295
  // ../core/src/adapters/antigravity.ts
2258
- import {
2259
- existsSync as existsSync3,
2260
- mkdirSync as mkdirSync2,
2261
- readdirSync as readdirSync3,
2262
- readFileSync as readFileSync5,
2263
- realpathSync,
2264
- statSync as statSync4,
2265
- unlinkSync as unlinkSync2,
2266
- writeFileSync as writeFileSync3
2267
- } from "node:fs";
2296
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync, statSync as statSync4 } from "node:fs";
2268
2297
  import path8 from "node:path";
2269
2298
  var NAME2 = "antigravity";
2270
2299
  var HOOK_NAME = "sessionpipe";
@@ -2296,10 +2325,8 @@ var antigravity = {
2296
2325
  const e = entry(cmd);
2297
2326
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
2298
2327
  hooks[HOOK_NAME] = e;
2299
- mkdirSync2(path8.dirname(file), { recursive: true });
2300
- writeFileSync3(file, formatJson(hooks, f));
2301
2328
  return [
2302
- {
2329
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
2303
2330
  file,
2304
2331
  changed: true,
2305
2332
  created: !f.existed,
@@ -2315,12 +2342,8 @@ var antigravity = {
2315
2342
  const hooks = f.value;
2316
2343
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
2317
2344
  delete hooks[HOOK_NAME];
2318
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
2319
- unlinkSync2(file);
2320
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
2321
- }
2322
- writeFileSync3(file, formatJson(hooks, f));
2323
- return [{ file, changed: true }];
2345
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
2346
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
2324
2347
  },
2325
2348
  installed(cmd, env = process.env) {
2326
2349
  const file = hooksFile(env);
@@ -3711,7 +3734,7 @@ function parseResult(stdout) {
3711
3734
  var isUuid = (s) => typeof s === "string" && UUID_RE3.test(s);
3712
3735
 
3713
3736
  // ../core/src/adapters/codex.ts
3714
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
3737
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
3715
3738
  import path11 from "node:path";
3716
3739
 
3717
3740
  // ../core/src/adapters/codex-config.ts
@@ -3864,29 +3887,30 @@ var configFile2 = (env) => path11.join(codexHome(env), "config.toml");
3864
3887
  var sessionsDir = (env) => path11.join(codexHome(env), "sessions");
3865
3888
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
3866
3889
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
3890
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
3867
3891
  function installNotify(cmd, env) {
3868
3892
  const file = configFile2(env);
3869
3893
  let text = "";
3894
+ let existed = true;
3870
3895
  try {
3871
3896
  text = readFileSync7(file, "utf8");
3872
- } catch {
3897
+ } catch (e) {
3898
+ existed = e.code !== "ENOENT";
3873
3899
  }
3874
3900
  const argv = cmd([NAME3, "notify"]).argv;
3875
3901
  const line2 = `notify = ${JSON.stringify(argv)}`;
3876
3902
  const n = readNotify(text);
3877
- const write = (next) => {
3878
- mkdirSync3(path11.dirname(file), { recursive: true });
3879
- writeFileSync4(file, next);
3903
+ const write = (next, note) => {
3904
+ const bad2 = writeBack(file, next, existed);
3905
+ return bad2 ? { ...bad2, note: `${bad2.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
3880
3906
  };
3881
3907
  if (n && !n.other) {
3882
3908
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
3883
3909
  return { file, changed: false, note: "notify current" };
3884
- write(withNotify(text, line2));
3885
- return {
3886
- file,
3887
- changed: true,
3888
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
3889
- };
3910
+ return write(
3911
+ withNotify(text, line2),
3912
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
3913
+ );
3890
3914
  }
3891
3915
  if (n?.other) {
3892
3916
  const prev = previousNotify(n.argv);
@@ -3895,20 +3919,20 @@ function installNotify(cmd, env) {
3895
3919
  if (JSON.stringify(prev) === JSON.stringify(argv))
3896
3920
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
3897
3921
  }
3898
- if (!prev || OURS2.test(JSON.stringify(prev))) {
3899
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
3900
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
3901
- }
3922
+ if (!prev || OURS2.test(JSON.stringify(prev)))
3923
+ return write(
3924
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
3925
+ "chained after Codex Computer Use (--previous-notify)"
3926
+ );
3902
3927
  }
3903
3928
  return {
3904
3929
  file,
3905
3930
  changed: false,
3906
3931
  skipped: true,
3907
- 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`
3932
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
3908
3933
  };
3909
3934
  }
3910
- write(withNotify(text, line2));
3911
- return { file, changed: true, note: "notify set at the top level" };
3935
+ return write(withNotify(text, line2), "notify set at the top level");
3912
3936
  }
3913
3937
  function uninstallNotify(env) {
3914
3938
  const file = configFile2(env);
@@ -3920,15 +3944,13 @@ function uninstallNotify(env) {
3920
3944
  }
3921
3945
  const n = readNotify(text);
3922
3946
  if (!n) return { file, changed: false };
3923
- if (!n.other) {
3924
- writeFileSync4(file, withoutNotify(text));
3925
- return { file, changed: true };
3926
- }
3947
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
3927
3948
  const prev = previousNotify(n.argv);
3928
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
3929
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
3930
- return { file, changed: true };
3931
- }
3949
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
3950
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
3951
+ file,
3952
+ changed: true
3953
+ };
3932
3954
  return { file, changed: false };
3933
3955
  }
3934
3956
  function findRollout(threadId, env = process.env) {
@@ -3960,8 +3982,12 @@ function rolloutMeta(file) {
3960
3982
  return scanLines(file, (line2) => parseLines([line2])[0] ?? {});
3961
3983
  }
3962
3984
  function ofAnotherThread(file, id) {
3985
+ const other = threadOf(file);
3986
+ return !!other && other !== id.toLowerCase();
3987
+ }
3988
+ function threadOf(file) {
3963
3989
  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);
3964
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
3990
+ return m?.[1] ? m[1].toLowerCase() : null;
3965
3991
  }
3966
3992
  var codex = {
3967
3993
  name: NAME3,
@@ -4040,6 +4066,8 @@ var codex = {
4040
4066
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
4041
4067
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
4042
4068
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
4069
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
4070
+ 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;
4043
4071
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
4044
4072
  const events = [];
4045
4073
  switch (event) {
@@ -4059,17 +4087,30 @@ var codex = {
4059
4087
  );
4060
4088
  break;
4061
4089
  case "PreToolUse":
4062
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
4090
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
4063
4091
  break;
4064
4092
  case "PostToolUse":
4065
4093
  if (tool)
4066
4094
  events.push(
4067
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
4095
+ ev("tool.ended", {
4096
+ tool,
4097
+ call_id,
4098
+ turn_id,
4099
+ agent_id,
4100
+ ok: true,
4101
+ input: s.tool_input,
4102
+ output: s.tool_response
4103
+ })
4068
4104
  );
4069
4105
  break;
4070
4106
  case "PermissionRequest":
4071
4107
  events.push(
4072
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
4108
+ ev("attention.needed", {
4109
+ attention_id: call_id ?? `perm-${Date.now()}`,
4110
+ kind: "permission",
4111
+ tool,
4112
+ agent_id
4113
+ })
4073
4114
  );
4074
4115
  break;
4075
4116
  case "Stop":
@@ -4727,7 +4768,7 @@ var CHAIN_LIMIT = 4e3;
4727
4768
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
4728
4769
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
4729
4770
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
4730
- UNION ALL
4771
+ UNION
4731
4772
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
4732
4773
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
4733
4774
  )
@@ -5005,7 +5046,7 @@ function drivableHarnesses(env = process.env) {
5005
5046
  }
5006
5047
 
5007
5048
  // ../core/src/config.ts
5008
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
5049
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync2 } from "node:fs";
5009
5050
  import path15 from "node:path";
5010
5051
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
5011
5052
  function readConfig(file = configFile()) {
@@ -5021,9 +5062,36 @@ function readConfig(file = configFile()) {
5021
5062
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
5022
5063
  }
5023
5064
  }
5065
+ function configProblem(file) {
5066
+ let text;
5067
+ try {
5068
+ text = readFileSync10(file, "utf8");
5069
+ } catch (e) {
5070
+ const code = e.code;
5071
+ return code === "ENOENT" ? void 0 : `could not be read (${code})`;
5072
+ }
5073
+ if (!text.trim()) return void 0;
5074
+ if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
5075
+ try {
5076
+ const j = JSON.parse(text);
5077
+ return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
5078
+ } catch (e) {
5079
+ const m = e.message;
5080
+ const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
5081
+ return `is not valid JSON${at ? ` at ${at}` : ""}`;
5082
+ }
5083
+ }
5084
+ function assertConfigWritable(file = configFile()) {
5085
+ const problem = configProblem(file);
5086
+ if (problem)
5087
+ throw new Error(
5088
+ `${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.`
5089
+ );
5090
+ }
5024
5091
  function writeConfig(c, file = configFile()) {
5025
- mkdirSync4(path15.dirname(file), { recursive: true, mode: 448 });
5026
- writeFileSync5(file, `${JSON.stringify(c, null, 2)}
5092
+ assertConfigWritable(file);
5093
+ mkdirSync2(path15.dirname(file), { recursive: true, mode: 448 });
5094
+ writeFileSync2(file, `${JSON.stringify(c, null, 2)}
5027
5095
  `, { mode: 384 });
5028
5096
  if (process.platform !== "win32" && existsSync8(file)) {
5029
5097
  try {
@@ -5087,19 +5155,20 @@ function makeEvent(a, ctx) {
5087
5155
  }
5088
5156
 
5089
5157
  // ../core/src/outbox.ts
5158
+ import { randomBytes as randomBytes3 } from "node:crypto";
5090
5159
  import {
5091
5160
  appendFileSync,
5092
5161
  closeSync as closeSync3,
5093
5162
  existsSync as existsSync9,
5094
- mkdirSync as mkdirSync5,
5163
+ mkdirSync as mkdirSync3,
5095
5164
  openSync as openSync3,
5096
5165
  readdirSync as readdirSync6,
5097
5166
  readFileSync as readFileSync11,
5098
5167
  readSync as readSync3,
5099
5168
  renameSync,
5100
5169
  statSync as statSync6,
5101
- unlinkSync as unlinkSync3,
5102
- writeFileSync as writeFileSync6
5170
+ unlinkSync as unlinkSync2,
5171
+ writeFileSync as writeFileSync3
5103
5172
  } from "node:fs";
5104
5173
  import path16 from "node:path";
5105
5174
  var READ_CHUNK = 1024 * 1024;
@@ -5129,8 +5198,8 @@ var Outbox = class {
5129
5198
  } catch {
5130
5199
  }
5131
5200
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
5132
- mkdirSync5(path16.dirname(f), { recursive: true, mode: 448 });
5133
- writeFileSync6(f, String(n + 1), { mode: 384 });
5201
+ mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
5202
+ writeFileSync3(f, String(n + 1), { mode: 384 });
5134
5203
  return n;
5135
5204
  }
5136
5205
  lastSeqInFile(ref) {
@@ -5148,7 +5217,7 @@ var Outbox = class {
5148
5217
  }
5149
5218
  append(ref, events) {
5150
5219
  if (!events.length) return;
5151
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
5220
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
5152
5221
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
5153
5222
  `, { mode: 384 });
5154
5223
  }
@@ -5218,34 +5287,106 @@ var Outbox = class {
5218
5287
  closeSync3(fd);
5219
5288
  }
5220
5289
  }
5221
- /** Files older than `days` are pruned; the cursor files with them. */
5290
+ /** Files older than `days` are pruned, and each sink's cursor into them. The seq
5291
+ * counter stays: a session resumed after the window counts on from where it was,
5292
+ * so a receiver's (harness, session, seq) dedup doesn't take its new events for
5293
+ * ones it already has, and the sinks read the new file from its start.
5294
+ * It all happens under the drain's `flush` lock, since a drain holds the whole
5295
+ * cursor map and writes it back: a file and its cursor keys go together, so a
5296
+ * session resumed in between can't keep a key past its new file's end. While a
5297
+ * drain runs, nothing is pruned (0); the next prune does it. It never throws: it
5298
+ * is a cleanup, and a hook's drain comes after it. */
5222
5299
  prune(days, now = Date.now()) {
5300
+ let release = null;
5301
+ try {
5302
+ release = takeNamedLock(this.root, "flush");
5303
+ } catch {
5304
+ }
5305
+ if (!release) return 0;
5223
5306
  let n = 0;
5224
- const cutoff = now - days * 864e5;
5225
- for (const ref of this.sessions()) {
5226
- const f = this.file(ref);
5227
- try {
5228
- if (statSync6(f).mtimeMs < cutoff) {
5229
- unlinkSync3(f);
5230
- try {
5231
- unlinkSync3(this.seqFile(ref));
5232
- } catch {
5307
+ try {
5308
+ const cutoff = now - days * 864e5;
5309
+ const aged = this.sessions().filter((ref) => {
5310
+ try {
5311
+ return statSync6(this.file(ref)).mtimeMs < cutoff;
5312
+ } catch {
5313
+ return false;
5314
+ }
5315
+ });
5316
+ this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
5317
+ for (const ref of aged) {
5318
+ const f = this.file(ref);
5319
+ try {
5320
+ if (statSync6(f).mtimeMs < cutoff) {
5321
+ unlinkSync2(f);
5322
+ n++;
5233
5323
  }
5234
- n++;
5324
+ } catch {
5235
5325
  }
5236
- } catch {
5237
5326
  }
5327
+ } catch {
5328
+ } finally {
5329
+ release();
5238
5330
  }
5239
5331
  return n;
5240
5332
  }
5333
+ /** A sink's cursor into a session file about to be pruned (`gone`), or one already
5334
+ * missing (an earlier prune, an older release, by hand) is removed. The caller
5335
+ * holds the `flush` lock. */
5336
+ dropStaleCursors(gone2) {
5337
+ let names = [];
5338
+ try {
5339
+ names = readdirSync6(path16.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
5340
+ } catch {
5341
+ }
5342
+ const stale = (c) => Object.keys(c).filter((k) => {
5343
+ if (gone2.has(k)) return true;
5344
+ const i = k.indexOf("/");
5345
+ return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
5346
+ });
5347
+ for (const n of names) {
5348
+ const s = new Cursors(this.root, n.slice(0, -5));
5349
+ const c = s.read();
5350
+ const keys = stale(c);
5351
+ if (!keys.length) continue;
5352
+ for (const k of keys) delete c[k];
5353
+ s.write(c);
5354
+ }
5355
+ }
5241
5356
  delete(ref) {
5357
+ this.dropCursors(ref);
5242
5358
  for (const f of [this.file(ref), this.seqFile(ref)]) {
5243
5359
  try {
5244
- unlinkSync3(f);
5360
+ unlinkSync2(f);
5245
5361
  } catch {
5246
5362
  }
5247
5363
  }
5248
5364
  }
5365
+ /** The key every sink's cursor holds for a session is removed with the session's
5366
+ * file: the key alone pointed into the old file, and a session that went on (still
5367
+ * running, or resumed later) had its new file's first events unsent until it grew
5368
+ * past that offset. Every file under <state>/cursors is taken (a sink since taken
5369
+ * out of the config still has one; one that doesn't parse reads as empty and is
5370
+ * left), by the key a drain writes (the file's own name, `safeId`) and by the ref
5371
+ * as given. The caller holds the drain's `flush` lock: a drain holds the whole
5372
+ * cursor map and writes it back, so a key dropped under it would come back. */
5373
+ dropCursors(ref) {
5374
+ const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
5375
+ let names = [];
5376
+ try {
5377
+ names = readdirSync6(path16.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
5378
+ } catch {
5379
+ return;
5380
+ }
5381
+ for (const n of names) {
5382
+ const s = new Cursors(this.root, n.slice(0, -5));
5383
+ const c = s.read();
5384
+ const had = Object.keys(c).filter((k) => keys.has(k));
5385
+ if (!had.length) continue;
5386
+ for (const k of had) delete c[k];
5387
+ s.write(c);
5388
+ }
5389
+ }
5249
5390
  };
5250
5391
  var Cursors = class {
5251
5392
  constructor(root, sink2) {
@@ -5257,18 +5398,21 @@ var Cursors = class {
5257
5398
  file() {
5258
5399
  return path16.join(this.root, "cursors", `${safeId(this.sink)}.json`);
5259
5400
  }
5401
+ /** The map; empty when the file is missing, cut off mid-write, or holds anything
5402
+ * but an object (`null`, a list), so a drain never indexes into those. */
5260
5403
  read() {
5261
5404
  try {
5262
- return JSON.parse(readFileSync11(this.file(), "utf8"));
5405
+ const c = JSON.parse(readFileSync11(this.file(), "utf8"));
5406
+ return c && typeof c === "object" && !Array.isArray(c) ? c : {};
5263
5407
  } catch {
5264
5408
  return {};
5265
5409
  }
5266
5410
  }
5267
5411
  write(c) {
5268
5412
  const f = this.file();
5269
- mkdirSync5(path16.dirname(f), { recursive: true, mode: 448 });
5413
+ mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
5270
5414
  const tmp = `${f}.${process.pid}.tmp`;
5271
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
5415
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
5272
5416
  renameSync(tmp, f);
5273
5417
  }
5274
5418
  key(ref) {
@@ -5280,25 +5424,54 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
5280
5424
  }
5281
5425
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
5282
5426
  const dir = path16.join(root, "locks");
5283
- mkdirSync5(dir, { recursive: true, mode: 448 });
5427
+ mkdirSync3(dir, { recursive: true, mode: 448 });
5284
5428
  const f = path16.join(dir, `${name}.lock`);
5429
+ const nonce = randomBytes3(8).toString("hex");
5430
+ const mine = `${process.pid} ${nonce}`;
5431
+ let body = null;
5285
5432
  try {
5286
- if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync11(f, "utf8")))) unlinkSync3(f);
5287
- else return null;
5433
+ body = readFileSync11(f, "utf8");
5434
+ const stale = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
5435
+ if (!stale) return null;
5288
5436
  } catch {
5437
+ body = null;
5438
+ }
5439
+ if (body !== null) {
5440
+ const aside = `${f}.${process.pid}.${nonce}.stale`;
5441
+ try {
5442
+ renameSync(f, aside);
5443
+ } catch {
5444
+ return null;
5445
+ }
5446
+ const moved = readFileSync11(aside, "utf8");
5447
+ unlinkSync2(aside);
5448
+ if (moved !== body) {
5449
+ try {
5450
+ writeFileSync3(f, moved, { flag: "wx" });
5451
+ } catch {
5452
+ }
5453
+ return null;
5454
+ }
5289
5455
  }
5290
5456
  try {
5291
- writeFileSync6(f, String(process.pid), { flag: "wx" });
5457
+ writeFileSync3(f, mine, { flag: "wx" });
5292
5458
  } catch {
5293
5459
  return null;
5294
5460
  }
5295
5461
  return () => {
5296
5462
  try {
5297
- unlinkSync3(f);
5463
+ if (readFileSync11(f, "utf8") === mine) unlinkSync2(f);
5298
5464
  } catch {
5299
5465
  }
5300
5466
  };
5301
5467
  }
5468
+ async function waitNamedLock(root, name, forMs, everyMs = 100) {
5469
+ for (let waited = 0; ; waited += everyMs) {
5470
+ const release = takeNamedLock(root, name);
5471
+ if (release || waited >= forMs) return release;
5472
+ await new Promise((r) => setTimeout(r, everyMs));
5473
+ }
5474
+ }
5302
5475
  function processAlive(pid) {
5303
5476
  if (!Number.isInteger(pid) || pid <= 0) return true;
5304
5477
  try {
@@ -5313,7 +5486,7 @@ function processAlive(pid) {
5313
5486
  import { createHash as createHash4 } from "node:crypto";
5314
5487
  var RULESET_PII = "pii@1";
5315
5488
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
5316
- var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
5489
+ 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;
5317
5490
  function looksPhone(s) {
5318
5491
  const digits = s.replace(/\D/g, "");
5319
5492
  if (digits.length < 7 || digits.length > 15) return false;
@@ -5324,7 +5497,7 @@ function looksPhone(s) {
5324
5497
  function reducePii(text, home) {
5325
5498
  if (!text) return text;
5326
5499
  let out2 = text.replace(EMAIL, "[email]");
5327
- out2 = out2.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
5500
+ out2 = out2.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
5328
5501
  if (home) {
5329
5502
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5330
5503
  out2 = out2.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -5538,10 +5711,10 @@ var DATA_ALLOW = {
5538
5711
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
5539
5712
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
5540
5713
  "files.changed": { 1: ["paths"] },
5541
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
5714
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
5542
5715
  "attention.cleared": { 0: ["attention_id", "how"] },
5543
5716
  "subagent.started": { 1: ["agent_id", "agent_type"] },
5544
- "subagent.ended": { 1: ["agent_id", "agent_type"] },
5717
+ "subagent.ended": { 1: ["agent_id", "agent_type", "background"] },
5545
5718
  "context.compacted": { 1: ["trigger"] },
5546
5719
  "limits.observed": { 1: ["windows", "plan", "account_id"] },
5547
5720
  "delegation.started": { 0: ["ask_id", "via", "harness", "to"], 1: ["text"] },
@@ -5629,7 +5802,7 @@ function shortId(harness, id) {
5629
5802
  }
5630
5803
 
5631
5804
  // ../core/src/sinks/file.ts
5632
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
5805
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
5633
5806
  import path17 from "node:path";
5634
5807
  var FileSink = class {
5635
5808
  constructor(name, file, tier, pii) {
@@ -5644,7 +5817,7 @@ var FileSink = class {
5644
5817
  pii;
5645
5818
  async send(events) {
5646
5819
  try {
5647
- mkdirSync6(path17.dirname(this.file), { recursive: true });
5820
+ mkdirSync4(path17.dirname(this.file), { recursive: true });
5648
5821
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
5649
5822
  `);
5650
5823
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -5678,9 +5851,9 @@ var StdoutSink = class {
5678
5851
  };
5679
5852
 
5680
5853
  // src/ask.ts
5681
- import { existsSync as existsSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync17, statSync as statSync8, unlinkSync as unlinkSync6, writeFileSync as writeFileSync12 } from "node:fs";
5854
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, statSync as statSync8, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "node:fs";
5682
5855
  import os12 from "node:os";
5683
- import path28 from "node:path";
5856
+ import path29 from "node:path";
5684
5857
 
5685
5858
  // src/connect.ts
5686
5859
  import path18 from "node:path";
@@ -5714,10 +5887,17 @@ function connectFolders(o) {
5714
5887
  }
5715
5888
  return picked.length ? { folders: picked, from: "recent" } : { folders: [], from: "none" };
5716
5889
  }
5890
+ function tierArg(argv, fallback) {
5891
+ const i = argv.indexOf("--tier");
5892
+ if (i < 0) return fallback;
5893
+ const v = argv[i + 1] ?? "";
5894
+ const n = /^-?\d+$/.test(v.trim()) ? Number(v) : Number.NaN;
5895
+ if (Number.isNaN(n)) throw new Error(`--tier takes 0, 1, 2 or 3${v ? `, not "${v}"` : ""}`);
5896
+ return Math.max(0, Math.min(3, n));
5897
+ }
5717
5898
  function connectTier(flag2, current) {
5718
- if (flag2 !== void 0 && flag2 !== "")
5719
- return Math.max(0, Math.min(3, Math.trunc(Number(flag2)) || 0));
5720
- return Math.max(2, Math.min(3, current ?? 0));
5899
+ if (flag2 !== void 0) return flag2;
5900
+ return Math.max(2, Math.min(3, Math.trunc(Number(current) || 0)));
5721
5901
  }
5722
5902
 
5723
5903
  // src/control/local.ts
@@ -5795,13 +5975,13 @@ function askMany(sock, req, onReply, timeoutMs) {
5795
5975
  }
5796
5976
 
5797
5977
  // src/control/pair.ts
5798
- import { randomBytes as randomBytes4 } from "node:crypto";
5799
- import { chmodSync as chmodSync3, closeSync as closeSync5, mkdirSync as mkdirSync9, openSync as openSync5 } from "node:fs";
5978
+ import { randomBytes as randomBytes5 } from "node:crypto";
5979
+ import { chmodSync as chmodSync3, closeSync as closeSync5, mkdirSync as mkdirSync7, openSync as openSync5 } from "node:fs";
5800
5980
  import path22 from "node:path";
5801
5981
 
5802
5982
  // ../core/src/service/index.ts
5803
5983
  import { execFileSync as execFileSync6 } from "node:child_process";
5804
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "node:fs";
5984
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
5805
5985
  import os8 from "node:os";
5806
5986
  import path20 from "node:path";
5807
5987
  var execQuiet = (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
@@ -5879,8 +6059,8 @@ function installService(o) {
5879
6059
  };
5880
6060
  const write = (file) => {
5881
6061
  if (o.dryRun) return;
5882
- mkdirSync7(path20.dirname(file.path), { recursive: true });
5883
- writeFileSync7(file.path, file.text);
6062
+ mkdirSync5(path20.dirname(file.path), { recursive: true });
6063
+ writeFileSync4(file.path, file.text);
5884
6064
  };
5885
6065
  if (platform === "darwin") {
5886
6066
  write(f.plist);
@@ -5931,7 +6111,7 @@ function uninstallService(o) {
5931
6111
  };
5932
6112
  const remove = (file) => {
5933
6113
  files.push(file);
5934
- if (!o.dryRun) unlinkSync4(file);
6114
+ if (!o.dryRun) unlinkSync3(file);
5935
6115
  };
5936
6116
  if (platform === "darwin" && existsSync10(f.plist.path)) {
5937
6117
  run("launchctl", ["bootout", `gui/${uid}/${o.label}`]);
@@ -5953,17 +6133,17 @@ import {
5953
6133
  closeSync as closeSync4,
5954
6134
  existsSync as existsSync11,
5955
6135
  fsyncSync,
5956
- mkdirSync as mkdirSync8,
6136
+ mkdirSync as mkdirSync6,
5957
6137
  openSync as openSync4,
5958
6138
  readFileSync as readFileSync12,
5959
6139
  renameSync as renameSync2,
5960
- writeFileSync as writeFileSync8
6140
+ writeFileSync as writeFileSync5
5961
6141
  } from "node:fs";
5962
6142
  import path21 from "node:path";
5963
6143
 
5964
6144
  // src/secrets.ts
5965
6145
  import { spawnSync } from "node:child_process";
5966
- import { createHash as createHash6, randomBytes as randomBytes3 } from "node:crypto";
6146
+ import { createHash as createHash6, randomBytes as randomBytes4 } from "node:crypto";
5967
6147
  var SERVICE2 = "sessionpipe";
5968
6148
  var defaultRunner = (cmd, args2, input) => {
5969
6149
  const r = spawnSync(cmd, args2, {
@@ -6006,8 +6186,8 @@ function secretDel(store, account, run = defaultRunner) {
6006
6186
  else run("secret-tool", ["clear", "service", SERVICE2, "account", account]);
6007
6187
  }
6008
6188
  function probe2(store, run = defaultRunner) {
6009
- const account = `probe:${randomBytes3(6).toString("hex")}`;
6010
- const ok = secretSet(store, account, randomBytes3(12).toString("base64url"), run);
6189
+ const account = `probe:${randomBytes4(6).toString("hex")}`;
6190
+ const ok = secretSet(store, account, randomBytes4(12).toString("base64url"), run);
6011
6191
  secretDel(store, account, run);
6012
6192
  return ok;
6013
6193
  }
@@ -6018,13 +6198,18 @@ function bestStore(env = process.env, platform = process.platform, run = default
6018
6198
  for (const s of candidates) if (probe2(s, run)) return s;
6019
6199
  return "file";
6020
6200
  }
6021
- var storeLabel = (s) => s === "keychain" ? "the macOS Keychain" : s === "secret-service" ? "the Secret Service keyring" : "a 0600 file";
6201
+ var storeLabel = (s, platform = process.platform) => s === "keychain" ? "the macOS Keychain" : s === "secret-service" ? "the Secret Service keyring" : platform === "win32" ? "a file under your profile" : "a 0600 file";
6202
+ var fileKept = (platform = process.platform) => platform === "win32" ? "your profile's permissions" : "0600";
6203
+ function keysFileNote(platform = process.platform, env = process.env) {
6204
+ if (platform === "win32") return "under your profile's permissions (Windows has no keychain sessionpipe uses)";
6205
+ return `at 0600 \u2014 no unlocked keychain in this session${env.SSH_CONNECTION ? " (an ssh shell)" : ""}`;
6206
+ }
6022
6207
  function sinkWithSecrets(s, run = defaultRunner) {
6023
6208
  if (!s.token_in) return s;
6024
6209
  const token = secretGet(s.token_in, sinkAccount(s.name), run);
6025
- if (!token) return null;
6026
6210
  const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
6027
- return { ...s, token, ...secret ? { secret } : {} };
6211
+ if (!token && !secret) return null;
6212
+ return { ...s, ...token ? { token } : {}, ...secret ? { secret } : {} };
6028
6213
  }
6029
6214
  function moveSinkSecrets(to, o = {}) {
6030
6215
  const run = o.run ?? defaultRunner;
@@ -6081,9 +6266,9 @@ var askOn = (c) => c.ask !== false;
6081
6266
  var controlFile = (env = process.env) => path21.join(path21.dirname(configFile(env)), "control.json");
6082
6267
  var controlState = (env = process.env) => path21.join(stateDir(env), "control");
6083
6268
  function writePrivate(file, value) {
6084
- mkdirSync8(path21.dirname(file), { recursive: true, mode: 448 });
6269
+ mkdirSync6(path21.dirname(file), { recursive: true, mode: 448 });
6085
6270
  const tmp = `${file}.${process.pid}.tmp`;
6086
- writeFileSync8(tmp, `${JSON.stringify(value, null, 2)}
6271
+ writeFileSync5(tmp, `${JSON.stringify(value, null, 2)}
6087
6272
  `, { mode: 384 });
6088
6273
  renameSync2(tmp, file);
6089
6274
  try {
@@ -6182,7 +6367,7 @@ var NonceStore = class {
6182
6367
  const t = this.seenAt.get(nonce);
6183
6368
  if (t !== void 0 && t > this.now() - NONCE_TTL_MS) return true;
6184
6369
  const at = this.now();
6185
- mkdirSync8(path21.dirname(this.file), { recursive: true, mode: 448 });
6370
+ mkdirSync6(path21.dirname(this.file), { recursive: true, mode: 448 });
6186
6371
  const fd = openSync4(this.file, "a", 384);
6187
6372
  try {
6188
6373
  appendFileSync3(fd, `${JSON.stringify({ n: nonce, t: at })}
@@ -6200,8 +6385,8 @@ var NonceStore = class {
6200
6385
  for (const [n, t] of this.seenAt) if (t <= cut2) this.seenAt.delete(n);
6201
6386
  if (!existsSync11(this.file) && !this.seenAt.size) return;
6202
6387
  const body = [...this.seenAt].map(([n, t]) => JSON.stringify({ n, t })).join("\n");
6203
- mkdirSync8(path21.dirname(this.file), { recursive: true, mode: 448 });
6204
- writeFileSync8(`${this.file}.tmp`, body ? `${body}
6388
+ mkdirSync6(path21.dirname(this.file), { recursive: true, mode: 448 });
6389
+ writeFileSync5(`${this.file}.tmp`, body ? `${body}
6205
6390
  ` : "", { mode: 384 });
6206
6391
  renameSync2(`${this.file}.tmp`, this.file);
6207
6392
  }
@@ -6230,13 +6415,13 @@ var AckOutbox = class {
6230
6415
  }
6231
6416
  }
6232
6417
  add(a) {
6233
- mkdirSync8(path21.dirname(this.file), { recursive: true, mode: 448 });
6418
+ mkdirSync6(path21.dirname(this.file), { recursive: true, mode: 448 });
6234
6419
  appendFileSync3(this.file, `${JSON.stringify(a)}
6235
6420
  `, { mode: 384 });
6236
6421
  }
6237
6422
  keep(left) {
6238
- mkdirSync8(path21.dirname(this.file), { recursive: true, mode: 448 });
6239
- writeFileSync8(`${this.file}.tmp`, left.map((a) => JSON.stringify(a)).join("\n") + (left.length ? "\n" : ""), {
6423
+ mkdirSync6(path21.dirname(this.file), { recursive: true, mode: 448 });
6424
+ writeFileSync5(`${this.file}.tmp`, left.map((a) => JSON.stringify(a)).join("\n") + (left.length ? "\n" : ""), {
6240
6425
  mode: 384
6241
6426
  });
6242
6427
  renameSync2(`${this.file}.tmp`, this.file);
@@ -6290,7 +6475,7 @@ var writeState = (file, s) => writePrivate(file, {
6290
6475
  });
6291
6476
 
6292
6477
  // src/control/pair.ts
6293
- var VERSION = true ? "0.13.0" : "0.0.0";
6478
+ var VERSION = true ? "0.13.2" : "0.0.0";
6294
6479
  var RENEW_FOR_MS = 60 * 6e4;
6295
6480
  async function json(r) {
6296
6481
  try {
@@ -6339,7 +6524,7 @@ async function pair(o) {
6339
6524
  throw new Error(
6340
6525
  `${o.url} needs a key to pair: add it as a sink first (\`sessionpipe sink add ${o.url} --token \u2026\`) or pass --token.`
6341
6526
  );
6342
- const pollKey = o.token ? null : randomBytes4(32).toString("base64url");
6527
+ const pollKey = o.token ? null : randomBytes5(32).toString("base64url");
6343
6528
  const cfg = readControl(o.env) ?? { name: o.name, folders: [], mode: o.mode, receivers: [] };
6344
6529
  cfg.name = o.name || cfg.name;
6345
6530
  cfg.mode = o.mode;
@@ -6502,7 +6687,7 @@ var spec = (o) => ({
6502
6687
  });
6503
6688
  function installService2(o) {
6504
6689
  const logDir = controlState(o.env);
6505
- mkdirSync9(logDir, { recursive: true, mode: 448 });
6690
+ mkdirSync7(logDir, { recursive: true, mode: 448 });
6506
6691
  const log2 = path22.join(logDir, "daemon.log");
6507
6692
  try {
6508
6693
  closeSync5(openSync5(log2, "a", 384));
@@ -6548,12 +6733,12 @@ ${RE_ARM}`;
6548
6733
 
6549
6734
  // src/runtime.ts
6550
6735
  import { execFileSync as execFileSync7 } from "node:child_process";
6551
- import { readFileSync as readFileSync16, realpathSync as realpathSync3 } from "node:fs";
6736
+ import { readFileSync as readFileSync17, realpathSync as realpathSync3 } from "node:fs";
6552
6737
  import os11 from "node:os";
6553
- import path27 from "node:path";
6738
+ import path28 from "node:path";
6554
6739
 
6555
6740
  // src/launcher.ts
6556
- import { accessSync as accessSync2, chmodSync as chmodSync4, constants as constants2, mkdirSync as mkdirSync10, readFileSync as readFileSync13, renameSync as renameSync3, rmSync, writeFileSync as writeFileSync9 } from "node:fs";
6741
+ import { accessSync as accessSync2, chmodSync as chmodSync4, constants as constants2, mkdirSync as mkdirSync8, readFileSync as readFileSync13, renameSync as renameSync3, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
6557
6742
  import os9 from "node:os";
6558
6743
  import path23 from "node:path";
6559
6744
  var LAUNCHER = "sessionpipe-hook";
@@ -6585,14 +6770,14 @@ function writeIfChanged(file, body, mode) {
6585
6770
  } catch {
6586
6771
  }
6587
6772
  const tmp = `${file}.${process.pid}.tmp`;
6588
- writeFileSync9(tmp, body, { mode });
6773
+ writeFileSync6(tmp, body, { mode });
6589
6774
  renameSync3(tmp, file);
6590
6775
  chmodSync4(file, mode);
6591
6776
  return true;
6592
6777
  }
6593
6778
  function installLauncher(o) {
6594
6779
  const dir = dataDir(o.env);
6595
- mkdirSync10(dir, { recursive: true, mode: 493 });
6780
+ mkdirSync8(dir, { recursive: true, mode: 493 });
6596
6781
  let changed = false;
6597
6782
  for (const f of COPIED)
6598
6783
  changed = writeIfChanged(path23.join(dir, f), readFileSync13(path23.join(o.distDir, f)), 420) || changed;
@@ -6650,19 +6835,19 @@ function launcherState(version, env = process.env) {
6650
6835
  import {
6651
6836
  appendFileSync as appendFileSync4,
6652
6837
  existsSync as existsSync12,
6653
- mkdirSync as mkdirSync12,
6838
+ mkdirSync as mkdirSync11,
6654
6839
  readdirSync as readdirSync7,
6655
- readFileSync as readFileSync15,
6656
- renameSync as renameSync5,
6840
+ readFileSync as readFileSync16,
6841
+ renameSync as renameSync6,
6657
6842
  statSync as statSync7,
6658
- unlinkSync as unlinkSync5,
6659
- writeFileSync as writeFileSync11
6843
+ unlinkSync as unlinkSync4,
6844
+ writeFileSync as writeFileSync9
6660
6845
  } from "node:fs";
6661
6846
  import os10 from "node:os";
6662
- import path25 from "node:path";
6847
+ import path26 from "node:path";
6663
6848
 
6664
6849
  // src/control/aliases.ts
6665
- import { chmodSync as chmodSync5, mkdirSync as mkdirSync11, readFileSync as readFileSync14, renameSync as renameSync4, writeFileSync as writeFileSync10 } from "node:fs";
6850
+ import { chmodSync as chmodSync5, mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync4, writeFileSync as writeFileSync7 } from "node:fs";
6666
6851
  import path24 from "node:path";
6667
6852
  var aliasFile = (controlDir) => path24.join(controlDir, "aliases.json");
6668
6853
  function readAliases(file) {
@@ -6681,9 +6866,9 @@ function readAliases(file) {
6681
6866
  }
6682
6867
  }
6683
6868
  function writeAliases(file, all) {
6684
- mkdirSync11(path24.dirname(file), { recursive: true, mode: 448 });
6869
+ mkdirSync9(path24.dirname(file), { recursive: true, mode: 448 });
6685
6870
  const tmp = `${file}.${process.pid}.tmp`;
6686
- writeFileSync10(tmp, `${JSON.stringify(all, null, 2)}
6871
+ writeFileSync7(tmp, `${JSON.stringify(all, null, 2)}
6687
6872
  `, { mode: 384 });
6688
6873
  renameSync4(tmp, file);
6689
6874
  try {
@@ -6697,6 +6882,76 @@ function receiverIdOf(all, harness, id) {
6697
6882
  return null;
6698
6883
  }
6699
6884
 
6885
+ // src/fold.ts
6886
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "node:fs";
6887
+ import path25 from "node:path";
6888
+ var TOOLS_MODES = ["all", "main", "beat"];
6889
+ var TOOL_FOLD_MS = 6e4;
6890
+ var KEEP_MS = 24 * 60 * 6e4;
6891
+ function isToolsMode(v) {
6892
+ return typeof v === "string" && TOOLS_MODES.includes(v);
6893
+ }
6894
+ function toolsMode(s) {
6895
+ if (isToolsMode(s.tools)) return s.tools;
6896
+ if (isToolsMode(s.receiver_tools)) return s.receiver_tools;
6897
+ return "all";
6898
+ }
6899
+ var agentOf = (e) => typeof e.data.agent_id === "string" ? e.data.agent_id : "";
6900
+ var thins = (mode, agent) => mode === "beat" || mode === "main" && agent !== "";
6901
+ var askKey = (key, agent) => `${key}\0ask\0${agent}`;
6902
+ function sendTool(e, mode, clock, key) {
6903
+ if (mode === "all") return true;
6904
+ const agent = agentOf(e);
6905
+ const t = Date.parse(e.time);
6906
+ if (e.type === "attention.needed") {
6907
+ if (thins(mode, agent)) clock[askKey(key, agent)] = Number.isFinite(t) ? t : 0;
6908
+ return true;
6909
+ }
6910
+ if (e.type !== "tool.started" && e.type !== "tool.ended" || !thins(mode, agent)) return true;
6911
+ if (e.type === "tool.started") return false;
6912
+ if (!Number.isFinite(t)) return true;
6913
+ const ask2 = askKey(key, agent);
6914
+ const asked = clock[ask2];
6915
+ if (asked !== void 0 && t >= asked) {
6916
+ delete clock[ask2];
6917
+ clock[key] = t;
6918
+ return true;
6919
+ }
6920
+ const last = clock[key];
6921
+ if (last !== void 0 && t > last - TOOL_FOLD_MS && t < last + TOOL_FOLD_MS) return false;
6922
+ clock[key] = t;
6923
+ return true;
6924
+ }
6925
+ var ToolClock = class {
6926
+ constructor(root, sink2) {
6927
+ this.root = root;
6928
+ this.sink = sink2;
6929
+ }
6930
+ root;
6931
+ sink;
6932
+ file() {
6933
+ return path25.join(this.root, "folds", `${safeId(this.sink)}.json`);
6934
+ }
6935
+ read() {
6936
+ try {
6937
+ const j = JSON.parse(readFileSync15(this.file(), "utf8"));
6938
+ return j && typeof j === "object" && !Array.isArray(j) ? j : {};
6939
+ } catch {
6940
+ return {};
6941
+ }
6942
+ }
6943
+ write(c) {
6944
+ const newest = Math.max(0, ...Object.values(c).filter(Number.isFinite));
6945
+ const kept2 = {};
6946
+ for (const [k, v] of Object.entries(c)) if (Number.isFinite(v) && v >= newest - KEEP_MS) kept2[k] = v;
6947
+ const f = this.file();
6948
+ mkdirSync10(path25.dirname(f), { recursive: true, mode: 448 });
6949
+ const tmp = `${f}.${process.pid}.tmp`;
6950
+ writeFileSync8(tmp, JSON.stringify(kept2), { mode: 384 });
6951
+ renameSync5(tmp, f);
6952
+ }
6953
+ };
6954
+
6700
6955
  // src/http-sink.ts
6701
6956
  import { createHash as createHash7, createHmac } from "node:crypto";
6702
6957
  var TIMEOUT_MS = 15e3;
@@ -6814,27 +7069,27 @@ async function tellDaemon(state2, job, session, cwd, transcript) {
6814
7069
  300
6815
7070
  );
6816
7071
  }
6817
- var VERSION2 = true ? "0.13.0" : "0.0.0";
7072
+ var VERSION2 = true ? "0.13.2" : "0.0.0";
6818
7073
  function enqueueJob(state2, job) {
6819
7074
  const jobs = jobsDir(state2);
6820
- mkdirSync12(jobs, { recursive: true, mode: 448 });
6821
- const f = path25.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
6822
- writeFileSync11(f, JSON.stringify(job), { mode: 384 });
7075
+ mkdirSync11(jobs, { recursive: true, mode: 448 });
7076
+ const f = path26.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
7077
+ writeFileSync9(f, JSON.stringify(job), { mode: 384 });
6823
7078
  return f;
6824
7079
  }
6825
7080
  function releaseJob(claimed) {
6826
7081
  if (!claimed) return;
6827
7082
  try {
6828
- unlinkSync5(claimed);
7083
+ unlinkSync4(claimed);
6829
7084
  } catch {
6830
7085
  }
6831
7086
  }
6832
7087
  function log(state2, line2) {
6833
7088
  try {
6834
- mkdirSync12(state2, { recursive: true, mode: 448 });
6835
- const f = path25.join(state2, "log");
7089
+ mkdirSync11(state2, { recursive: true, mode: 448 });
7090
+ const f = path26.join(state2, "log");
6836
7091
  try {
6837
- if (statSync7(f).size > 1024 * 1024) renameSync5(f, `${f}.1`);
7092
+ if (statSync7(f).size > 1024 * 1024) renameSync6(f, `${f}.1`);
6838
7093
  } catch {
6839
7094
  }
6840
7095
  appendFileSync4(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line2}
@@ -6843,11 +7098,11 @@ function log(state2, line2) {
6843
7098
  }
6844
7099
  }
6845
7100
  function factsState(state2) {
6846
- const file = (h, s) => path25.join(state2, "facts", safeId(h), `${safeId(s)}.json`);
7101
+ const file = (h, s) => path26.join(state2, "facts", safeId(h), `${safeId(s)}.json`);
6847
7102
  return {
6848
7103
  get(h, s) {
6849
7104
  try {
6850
- return JSON.parse(readFileSync15(file(h, s), "utf8"));
7105
+ return JSON.parse(readFileSync16(file(h, s), "utf8"));
6851
7106
  } catch {
6852
7107
  return {};
6853
7108
  }
@@ -6856,8 +7111,8 @@ function factsState(state2) {
6856
7111
  const cur = this.get(h, s);
6857
7112
  const next = { ...cur, ...patch };
6858
7113
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
6859
- mkdirSync12(path25.dirname(file(h, s)), { recursive: true, mode: 448 });
6860
- writeFileSync11(file(h, s), JSON.stringify(next), { mode: 384 });
7114
+ mkdirSync11(path26.dirname(file(h, s)), { recursive: true, mode: 448 });
7115
+ writeFileSync9(file(h, s), JSON.stringify(next), { mode: 384 });
6861
7116
  }
6862
7117
  };
6863
7118
  }
@@ -6870,7 +7125,7 @@ function buildSinks(sinks, o = {}) {
6870
7125
  o.locked?.(raw.name);
6871
7126
  continue;
6872
7127
  }
6873
- const tier = Math.min(s.tier, s.max_tier ?? 3);
7128
+ const tier = Math.trunc(Math.min(Number(s.tier) || 0, s.max_tier ?? 3));
6874
7129
  if (s.url === "stdout") out2.push(new StdoutSink(s.name, tier, !!s.pii));
6875
7130
  else if (s.url.startsWith("file:"))
6876
7131
  out2.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os10.homedir()), tier, !!s.pii));
@@ -6932,7 +7187,7 @@ async function runJob(job, opts = {}) {
6932
7187
  );
6933
7188
  return { events: [], delivered: {} };
6934
7189
  }
6935
- const id = receiverIdOf(readAliases(aliasFile(path25.join(state2, "control"))), job.harness, r.session.id) ?? r.session.id;
7190
+ const id = receiverIdOf(readAliases(aliasFile(path26.join(state2, "control"))), job.harness, r.session.id) ?? r.session.id;
6936
7191
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
6937
7192
  if (!claudeEnd) await tellDaemon(state2, job, id, r.session.cwd, r.transcript);
6938
7193
  const outbox = new Outbox(state2);
@@ -6969,7 +7224,7 @@ async function runJob(job, opts = {}) {
6969
7224
  learned = rest;
6970
7225
  if (harness_version) facts.save(job.harness, id, { harness_version });
6971
7226
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
6972
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path25.isAbsolute(r.session.cwd))
7227
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path26.isAbsolute(r.session.cwd))
6973
7228
  facts.save(job.harness, id, { cwd: r.session.cwd });
6974
7229
  }
6975
7230
  const known = mem.session ?? {};
@@ -7087,8 +7342,12 @@ async function runJob(job, opts = {}) {
7087
7342
  } finally {
7088
7343
  release();
7089
7344
  }
7090
- const delivered = await drain(state2, cfg.sinks, outbox, ref) ?? {};
7091
7345
  if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
7346
+ const delivered = opts.drain === false ? {} : await drain(state2, cfg.sinks, outbox, ref, {
7347
+ ...opts.clock ? { clock: opts.clock } : {},
7348
+ ...opts.deadline !== void 0 ? { deadline: opts.deadline } : {},
7349
+ ...opts.handoff ? { handoff: opts.handoff } : {}
7350
+ }) ?? {};
7092
7351
  return { events, delivered };
7093
7352
  }
7094
7353
  async function appendObserved(state2, harness, id, data, opts = {}) {
@@ -7127,25 +7386,75 @@ async function appendObserved(state2, harness, id, data, opts = {}) {
7127
7386
  await drain(state2, cfg.sinks, outbox, ref);
7128
7387
  return e;
7129
7388
  }
7130
- async function drain(state2, sinkConfigs, outbox, first) {
7131
- const more = path25.join(state2, "locks", "flush.more");
7389
+ var REAL_CLOCK = { now: () => Date.now(), sleep: (ms2) => new Promise((r) => setTimeout(r, ms2)) };
7390
+ var PACE_MS = 1e3;
7391
+ var sentFile = (state2) => path26.join(state2, "locks", "flush.sent");
7392
+ function lastSent(state2) {
7393
+ try {
7394
+ return Number(readFileSync16(sentFile(state2), "utf8"));
7395
+ } catch {
7396
+ return 0;
7397
+ }
7398
+ }
7399
+ function noteSent(state2, at) {
7400
+ try {
7401
+ mkdirSync11(path26.dirname(sentFile(state2)), { recursive: true, mode: 448 });
7402
+ writeFileSync9(sentFile(state2), String(at), { mode: 384 });
7403
+ } catch {
7404
+ }
7405
+ }
7406
+ function waitingForReceiver(state2, sinkConfigs, outbox, now) {
7407
+ const receivers = sinkConfigs.filter(
7408
+ (s) => !s.paused && /^https?:\/\//.test(s.url) && !((readBackoff(state2, s.name)?.until ?? 0) > now)
7409
+ );
7410
+ if (!receivers.length) return false;
7411
+ const refs = outbox.sessions();
7412
+ for (const s of receivers) {
7413
+ const cursors = new Cursors(state2, s.name);
7414
+ const c = cursors.read();
7415
+ for (const ref of refs) {
7416
+ try {
7417
+ if (statSync7(outbox.file(ref)).size > (c[cursors.key(ref)] ?? 0)) return true;
7418
+ } catch {
7419
+ }
7420
+ }
7421
+ }
7422
+ return false;
7423
+ }
7424
+ var PASS_BUDGET_MS = 3e3;
7425
+ var moreFile = (state2) => path26.join(state2, "locks", "flush.more");
7426
+ function markMore(state2) {
7427
+ try {
7428
+ mkdirSync11(path26.dirname(moreFile(state2)), { recursive: true, mode: 448 });
7429
+ writeFileSync9(moreFile(state2), "", { mode: 384 });
7430
+ } catch {
7431
+ }
7432
+ }
7433
+ async function drain(state2, sinkConfigs, outbox, first, o = {}) {
7434
+ const clock = o.clock ?? REAL_CLOCK;
7435
+ const more = moreFile(state2);
7132
7436
  let release = takeNamedLock(state2, "flush");
7133
7437
  if (!release) {
7134
- try {
7135
- writeFileSync11(more, "", { mode: 384 });
7136
- } catch {
7137
- }
7438
+ markMore(state2);
7138
7439
  release = takeNamedLock(state2, "flush");
7139
7440
  if (!release) return null;
7140
7441
  }
7141
7442
  const delivered = {};
7142
7443
  for (; ; ) {
7143
- try {
7144
- unlinkSync5(more);
7145
- } catch {
7444
+ if (o.deadline !== void 0 && clock.now() + PASS_BUDGET_MS > o.deadline) {
7445
+ markMore(state2);
7446
+ release();
7447
+ o.handoff?.();
7448
+ return delivered;
7146
7449
  }
7147
7450
  try {
7148
- for (const [k, v] of Object.entries(await flush(state2, sinkConfigs, outbox, first)))
7451
+ const wait = Math.min(PACE_MS, lastSent(state2) + PACE_MS - clock.now());
7452
+ if (wait > 0 && waitingForReceiver(state2, sinkConfigs, outbox, clock.now())) await clock.sleep(wait);
7453
+ try {
7454
+ unlinkSync4(more);
7455
+ } catch {
7456
+ }
7457
+ for (const [k, v] of Object.entries(await flush(state2, sinkConfigs, outbox, first, { clock })))
7149
7458
  delivered[k] = (delivered[k] ?? 0) + v;
7150
7459
  } finally {
7151
7460
  release();
@@ -7156,10 +7465,10 @@ async function drain(state2, sinkConfigs, outbox, first) {
7156
7465
  }
7157
7466
  }
7158
7467
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
7159
- var backoffFile = (state2, sink2) => path25.join(state2, "backoff", `${safeId(sink2)}.json`);
7468
+ var backoffFile = (state2, sink2) => path26.join(state2, "backoff", `${safeId(sink2)}.json`);
7160
7469
  function readBackoff(state2, sink2) {
7161
7470
  try {
7162
- const b = JSON.parse(readFileSync15(backoffFile(state2, sink2), "utf8"));
7471
+ const b = JSON.parse(readFileSync16(backoffFile(state2, sink2), "utf8"));
7163
7472
  return typeof b.until === "number" ? b : null;
7164
7473
  } catch {
7165
7474
  return null;
@@ -7172,15 +7481,15 @@ function noteFailure(state2, sink2, res, prev) {
7172
7481
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
7173
7482
  const f = backoffFile(state2, sink2);
7174
7483
  try {
7175
- mkdirSync12(path25.dirname(f), { recursive: true, mode: 448 });
7176
- writeFileSync11(f, JSON.stringify(b), { mode: 384 });
7484
+ mkdirSync11(path26.dirname(f), { recursive: true, mode: 448 });
7485
+ writeFileSync9(f, JSON.stringify(b), { mode: 384 });
7177
7486
  } catch {
7178
7487
  }
7179
7488
  return b;
7180
7489
  }
7181
7490
  function clearBackoff(state2, sink2) {
7182
7491
  try {
7183
- unlinkSync5(backoffFile(state2, sink2));
7492
+ unlinkSync4(backoffFile(state2, sink2));
7184
7493
  } catch {
7185
7494
  }
7186
7495
  }
@@ -7191,14 +7500,31 @@ var BATCH = 50;
7191
7500
  var MAX_PER_RUN = 500;
7192
7501
  async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7193
7502
  const delivered = {};
7503
+ const clock = o.clock ?? REAL_CLOCK;
7194
7504
  const sinks = buildSinks(sinkConfigs, {
7195
7505
  locked: (name) => log(state2, `sink ${name}: its token is in a keychain this process can't read; left for later`)
7196
7506
  });
7507
+ const modes = new Map(sinkConfigs.map((s) => [s.name, toolsMode(s)]));
7197
7508
  const refs = outbox.sessions();
7198
7509
  if (first) {
7199
7510
  const k = `${first.harness}/${safeId(first.session)}`;
7200
7511
  refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
7201
7512
  }
7513
+ const unreadable = /* @__PURE__ */ new Set();
7514
+ const readOrSkip = (ref, offset, room) => {
7515
+ try {
7516
+ return outbox.read(ref, offset, room);
7517
+ } catch (e) {
7518
+ const code = e.code;
7519
+ if (typeof code !== "string") throw e;
7520
+ const f = outbox.file(ref);
7521
+ if (!unreadable.has(f)) {
7522
+ unreadable.add(f);
7523
+ log(state2, `drain: can't read ${f} (${code}); skipped, its cursors stay`);
7524
+ }
7525
+ return null;
7526
+ }
7527
+ };
7202
7528
  const drainSink = async (sink2) => {
7203
7529
  let backoff = readBackoff(state2, sink2.name);
7204
7530
  if (backoff && !o.force && backoff.until > Date.now()) {
@@ -7207,11 +7533,16 @@ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7207
7533
  }
7208
7534
  const cursors = new Cursors(state2, sink2.name);
7209
7535
  const c = cursors.read();
7536
+ const mode = modes.get(sink2.name);
7537
+ const tools = new ToolClock(state2, sink2.name);
7538
+ let clockNow = mode === "all" ? {} : tools.read();
7539
+ let clockNext = { ...clockNow };
7210
7540
  let sent = 0;
7211
7541
  let stalled = false;
7212
7542
  let batch = [];
7213
7543
  let pending = {};
7214
7544
  const deliver = async (events) => {
7545
+ if (sink2 instanceof HttpSink) noteSent(state2, clock.now());
7215
7546
  const res = await sink2.send(events);
7216
7547
  if (res.ok || res.action !== "split") return res;
7217
7548
  if (events.length === 1) {
@@ -7219,10 +7550,20 @@ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7219
7550
  log(state2, `sink ${sink2.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
7220
7551
  return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
7221
7552
  }
7553
+ const part = async (some) => {
7554
+ const r = await deliver(some);
7555
+ if (r.ok || r.action !== "drop") return r;
7556
+ log(
7557
+ state2,
7558
+ `sink ${sink2.name}: ${r.status ?? ""} ${r.error ?? ""} \u2192 drop, ${some.length} of ${events.length} events`
7559
+ );
7560
+ const rejected = some.map((e) => ({ id: e.id, reason: "dropped" }));
7561
+ return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected };
7562
+ };
7222
7563
  const half = Math.ceil(events.length / 2);
7223
- const a = await deliver(events.slice(0, half));
7564
+ const a = await part(events.slice(0, half));
7224
7565
  if (!a.ok) return a;
7225
- const b = await deliver(events.slice(half));
7566
+ const b = await part(events.slice(half));
7226
7567
  if (!b.ok) return b;
7227
7568
  return {
7228
7569
  ok: true,
@@ -7232,11 +7573,17 @@ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7232
7573
  rejected: [...a.rejected ?? [], ...b.rejected ?? []]
7233
7574
  };
7234
7575
  };
7576
+ const commitClock = () => {
7577
+ if (mode === "all" || JSON.stringify(clockNext) === JSON.stringify(clockNow)) return;
7578
+ tools.write(clockNext);
7579
+ clockNow = { ...clockNext };
7580
+ };
7235
7581
  const send = async () => {
7236
7582
  if (!batch.length) {
7237
7583
  Object.assign(c, pending);
7238
7584
  pending = {};
7239
7585
  cursors.write(c);
7586
+ commitClock();
7240
7587
  return true;
7241
7588
  }
7242
7589
  const res = await deliver(batch);
@@ -7252,6 +7599,7 @@ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7252
7599
  if (res.action !== "drop") {
7253
7600
  batch = [];
7254
7601
  pending = {};
7602
+ clockNext = { ...clockNow };
7255
7603
  return false;
7256
7604
  }
7257
7605
  } else if (backoff) {
@@ -7261,6 +7609,7 @@ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7261
7609
  sent += batch.length;
7262
7610
  Object.assign(c, pending);
7263
7611
  cursors.write(c);
7612
+ commitClock();
7264
7613
  batch = [];
7265
7614
  pending = {};
7266
7615
  return true;
@@ -7270,11 +7619,15 @@ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
7270
7619
  let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
7271
7620
  for (; ; ) {
7272
7621
  const room = BATCH - batch.length;
7273
- const { events, offset: next } = outbox.read(ref, offset, room);
7622
+ const read = readOrSkip(ref, offset, room);
7623
+ if (!read) break;
7624
+ const { events, offset: next } = read;
7274
7625
  if (!events.length) break;
7275
7626
  for (const e of events) {
7276
7627
  const f = filterEvent(e, sink2.tier, sink2.pii, { home: os10.homedir() });
7277
- if (f) batch.push(f);
7628
+ if (!f) continue;
7629
+ if (sendTool(f, mode, clockNext, cursors.key(ref))) batch.push(f);
7630
+ else if (o.thinned) o.thinned[sink2.name] = (o.thinned[sink2.name] ?? 0) + 1;
7278
7631
  }
7279
7632
  offset = next;
7280
7633
  pending[cursors.key(ref)] = offset;
@@ -7306,12 +7659,12 @@ function applySinkVerdict(name, res) {
7306
7659
  } catch {
7307
7660
  }
7308
7661
  }
7309
- var jobsDir = (state2) => path25.join(state2, "jobs");
7662
+ var jobsDir = (state2) => path26.join(state2, "jobs");
7310
7663
 
7311
7664
  // src/update.ts
7312
7665
  import { spawn as spawn4 } from "node:child_process";
7313
7666
  import { accessSync as accessSync3, constants as constants3, existsSync as existsSync13, realpathSync as realpathSync2 } from "node:fs";
7314
- import path26 from "node:path";
7667
+ import path27 from "node:path";
7315
7668
 
7316
7669
  // ../core/src/version.ts
7317
7670
  import { spawn as spawn3 } from "node:child_process";
@@ -7399,11 +7752,11 @@ function globalPrefix(distDir2, platform = process.platform) {
7399
7752
  if (viaNpx(distDir2)) return null;
7400
7753
  const prefix = prefixOf(distDir2, platform);
7401
7754
  if (!prefix) return null;
7402
- const bin = platform === "win32" ? path26.join(prefix, "sessionpipe.cmd") : path26.join(prefix, "bin", "sessionpipe");
7755
+ const bin = platform === "win32" ? path27.join(prefix, "sessionpipe.cmd") : path27.join(prefix, "bin", "sessionpipe");
7403
7756
  return existsSync13(bin) ? prefix : null;
7404
7757
  }
7405
7758
  function unwritable(prefix, platform = process.platform) {
7406
- const dir = platform === "win32" ? path26.join(prefix, "node_modules") : path26.join(prefix, "lib", "node_modules");
7759
+ const dir = platform === "win32" ? path27.join(prefix, "node_modules") : path27.join(prefix, "lib", "node_modules");
7407
7760
  try {
7408
7761
  accessSync3(dir, constants3.W_OK);
7409
7762
  return null;
@@ -7412,8 +7765,8 @@ function unwritable(prefix, platform = process.platform) {
7412
7765
  }
7413
7766
  }
7414
7767
  function npmArgv(node, platform = process.platform) {
7415
- const dir = path26.dirname(node);
7416
- const candidates = platform === "win32" ? [path26.join(dir, "node_modules", "npm", "bin", "npm-cli.js")] : [path26.join(dir, "npm"), path26.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
7768
+ const dir = path27.dirname(node);
7769
+ const candidates = platform === "win32" ? [path27.join(dir, "node_modules", "npm", "bin", "npm-cli.js")] : [path27.join(dir, "npm"), path27.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
7417
7770
  for (const c of candidates) {
7418
7771
  try {
7419
7772
  const real = realpathSync2(c);
@@ -7458,7 +7811,7 @@ var execCapture = (argv, o) => new Promise((resolve) => {
7458
7811
  });
7459
7812
  var lastLine = (r) => (r.error || r.stderr.trim().split("\n").filter(Boolean).slice(-2).join(" ") || `exit ${r.code}`).slice(0, 300);
7460
7813
  async function installedVersion(o) {
7461
- const r = await (o.exec ?? execCapture)([o.node, path26.join(o.distDir, "cli.js"), "--version"], {
7814
+ const r = await (o.exec ?? execCapture)([o.node, path27.join(o.distDir, "cli.js"), "--version"], {
7462
7815
  env: o.env ?? process.env,
7463
7816
  timeoutMs: 3e4
7464
7817
  });
@@ -7470,7 +7823,7 @@ async function installVersion(version, o) {
7470
7823
  const platform = o.platform ?? process.platform;
7471
7824
  const exec = o.exec ?? execCapture;
7472
7825
  const base = o.env ?? process.env;
7473
- const env = { ...base, PATH: [path26.dirname(o.node), base.PATH].filter(Boolean).join(path26.delimiter) };
7826
+ const env = { ...base, PATH: [path27.dirname(o.node), base.PATH].filter(Boolean).join(path27.delimiter) };
7474
7827
  const prefix = globalPrefix(o.distDir, platform);
7475
7828
  if (!prefix)
7476
7829
  return { ok: false, ms: Date.now() - t0, stage: "prefix", error: "this copy isn't a global npm install" };
@@ -7501,7 +7854,7 @@ async function installVersion(version, o) {
7501
7854
  let hooks = "no harness set up here; nothing to re-arm";
7502
7855
  if (o.harnesses.length) {
7503
7856
  const h = await exec(
7504
- [o.node, path26.join(o.distDir, "cli.js"), "install", "--backfill", "0", ...o.harnesses.map((n) => `--${n}`)],
7857
+ [o.node, path27.join(o.distDir, "cli.js"), "install", "--backfill", "0", ...o.harnesses.map((n) => `--${n}`)],
7505
7858
  { env, timeoutMs: 2 * 6e4 }
7506
7859
  );
7507
7860
  hooks = h.code === 0 ? `hooks re-armed for ${o.harnesses.join(", ")}` : `re-arming the hooks failed (${lastLine(h)}); \`sessionpipe install\` does it`;
@@ -7542,7 +7895,7 @@ function installLocation() {
7542
7895
  }
7543
7896
  return {
7544
7897
  prefix,
7545
- target: /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path27.join(os11.homedir(), ".local") : prefix
7898
+ target: /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path28.join(os11.homedir(), ".local") : prefix
7546
7899
  };
7547
7900
  }
7548
7901
  function assertSetupVersion(allowDowngrade, target2) {
@@ -7557,7 +7910,7 @@ function assertSetupVersion(allowDowngrade, target2) {
7557
7910
  check(hooks.version, hooks.file);
7558
7911
  const prefix = target2 ?? installLocation().target;
7559
7912
  if (!prefix) return;
7560
- const pkg = path27.join(
7913
+ const pkg = path28.join(
7561
7914
  prefix,
7562
7915
  ...process.platform === "win32" ? [] : ["lib"],
7563
7916
  "node_modules",
@@ -7566,7 +7919,7 @@ function assertSetupVersion(allowDowngrade, target2) {
7566
7919
  );
7567
7920
  let installed;
7568
7921
  try {
7569
- installed = JSON.parse(readFileSync16(pkg, "utf8")).version;
7922
+ installed = JSON.parse(readFileSync17(pkg, "utf8")).version;
7570
7923
  } catch {
7571
7924
  }
7572
7925
  if (typeof installed === "string") check(installed, pkg);
@@ -7580,9 +7933,9 @@ function rerunGlobally(argv, out2, setup2) {
7580
7933
  execFileSync7("npm", ["install", "-g", `sessionpipe@${VERSION2}`, ...target2 ? ["--prefix", target2] : []], {
7581
7934
  stdio: ["ignore", "ignore", "inherit"]
7582
7935
  });
7583
- bin = process.platform === "win32" ? path27.join(target2, "sessionpipe.cmd") : target2 ? path27.join(target2, "bin", "sessionpipe") : "sessionpipe";
7936
+ bin = process.platform === "win32" ? path28.join(target2, "sessionpipe.cmd") : target2 ? path28.join(target2, "bin", "sessionpipe") : "sessionpipe";
7584
7937
  if (target2 && target2 !== prefix)
7585
- out2(` Installed into ${target2}; make sure ${path27.join(target2, "bin")} is on your PATH.`);
7938
+ out2(` Installed into ${target2}; make sure ${path28.join(target2, "bin")} is on your PATH.`);
7586
7939
  } catch {
7587
7940
  out2(
7588
7941
  " ! couldn't install globally (`npm install -g sessionpipe` failed). Run it yourself, then retry this command."
@@ -7663,7 +8016,7 @@ async function askOnce(rest, flags, io, env, harnesses) {
7663
8016
  session = target2;
7664
8017
  } else if (!HARNESS.test(target2)) return usage(`${target2} isn't an agent's name (claude-code, codex, gemini-cli, \u2026)`);
7665
8018
  const given = flags["--text"];
7666
- const text = given === "-" ? (io.stdin ?? (() => readFileSync17(0, "utf8")))() : typeof given === "string" ? given : rest.slice(1).join(" ");
8019
+ const text = given === "-" ? (io.stdin ?? (() => readFileSync18(0, "utf8")))() : typeof given === "string" ? given : rest.slice(1).join(" ");
7667
8020
  if (!text.trim()) return usage("nothing to ask: put the task in quotes after the agent, or pipe it with `--text -`");
7668
8021
  if (text.length > TEXT_MAX2)
7669
8022
  return usage(
@@ -7674,7 +8027,7 @@ async function askOnce(rest, flags, io, env, harnesses) {
7674
8027
  return usage("--permission is read or safe: an asked agent never runs wider than this machine's mode");
7675
8028
  const model = flags["--model"];
7676
8029
  if (model === "") return usage("--model needs a name");
7677
- const cwd = path28.resolve(typeof flags["--cwd"] === "string" ? flags["--cwd"] : process.cwd());
8030
+ const cwd = path29.resolve(typeof flags["--cwd"] === "string" ? flags["--cwd"] : process.cwd());
7678
8031
  const from = waitSession(void 0, env) ?? void 0;
7679
8032
  const chain = (env.SESSIONPIPE_ASK_CHAIN ?? "").split(",").map((s) => s.trim()).filter(Boolean);
7680
8033
  const req = {
@@ -7799,9 +8152,9 @@ function on(argv, io, env, distDir2, harnesses, noService, yes) {
7799
8152
  );
7800
8153
  if (noService) io.out(" Daemon: not installed as a service (--no-service); run `sessionpipe control run` yourself");
7801
8154
  else {
7802
- let cli = path28.join(distDir2 ?? ".", "cli.js");
8155
+ let cli = path29.join(distDir2 ?? ".", "cli.js");
7803
8156
  try {
7804
- cli = statSync8(cli).isFile() ? path28.resolve(cli) : cli;
8157
+ cli = statSync8(cli).isFile() ? path29.resolve(cli) : cli;
7805
8158
  } catch {
7806
8159
  }
7807
8160
  io.out(` Daemon: ${installService2({ node: stableNode(process.execPath), cli, env })}`);
@@ -7861,8 +8214,8 @@ ${ASK_CLOSE}`;
7861
8214
  function setupFiles(env = process.env) {
7862
8215
  const home = env.HOME || homeDir();
7863
8216
  return [
7864
- ...claudeDirs(env, home).map((d) => path28.join(d, "CLAUDE.md")),
7865
- path28.join(env.CODEX_HOME || path28.join(home, ".codex"), "AGENTS.md")
8217
+ ...claudeDirs(env, home).map((d) => path29.join(d, "CLAUDE.md")),
8218
+ path29.join(env.CODEX_HOME || path29.join(home, ".codex"), "AGENTS.md")
7866
8219
  ];
7867
8220
  }
7868
8221
  var sectionAt = (text) => {
@@ -7891,8 +8244,8 @@ function setupAsk(env, agents, remove) {
7891
8244
  const section = askSection(agents);
7892
8245
  const out2 = [];
7893
8246
  for (const file of setupFiles(env)) {
7894
- const had = existsSync14(file) ? readFileSync17(file, "utf8") : null;
7895
- if (had === null && (remove || !existsSync14(path28.dirname(file)))) {
8247
+ const had = existsSync14(file) ? readFileSync18(file, "utf8") : null;
8248
+ if (had === null && (remove || !existsSync14(path29.dirname(file)))) {
7896
8249
  out2.push({ file, did: remove ? "absent" : "skipped" });
7897
8250
  continue;
7898
8251
  }
@@ -7900,8 +8253,8 @@ function setupAsk(env, agents, remove) {
7900
8253
  const next2 = withoutAskSection(had);
7901
8254
  if (next2 === had) out2.push({ file, did: "absent" });
7902
8255
  else {
7903
- if (next2 === "") unlinkSync6(file);
7904
- else writeFileSync12(file, next2);
8256
+ if (next2 === "") unlinkSync5(file);
8257
+ else writeFileSync10(file, next2);
7905
8258
  out2.push({ file, did: "removed" });
7906
8259
  }
7907
8260
  continue;
@@ -7909,8 +8262,8 @@ function setupAsk(env, agents, remove) {
7909
8262
  const next = withAskSection(had, section);
7910
8263
  if (next === had) out2.push({ file, did: "unchanged" });
7911
8264
  else {
7912
- mkdirSync13(path28.dirname(file), { recursive: true });
7913
- writeFileSync12(file, next);
8265
+ mkdirSync12(path29.dirname(file), { recursive: true });
8266
+ writeFileSync10(file, next);
7914
8267
  out2.push({ file, did: had === null ? "added" : "updated" });
7915
8268
  }
7916
8269
  }
@@ -7934,15 +8287,15 @@ function setup(io, env, harnesses, remove) {
7934
8287
  }
7935
8288
 
7936
8289
  // src/control/autoupdate.ts
7937
- import { readFileSync as readFileSync18 } from "node:fs";
7938
- import path29 from "node:path";
8290
+ import { readFileSync as readFileSync19 } from "node:fs";
8291
+ import path30 from "node:path";
7939
8292
  var DAY_MS = 24 * 36e5;
7940
8293
  var LABEL4 = "org.sessionpipe.control";
7941
8294
  var UNIT2 = "sessionpipe-control.service";
7942
- var updateFile = (env = process.env) => path29.join(controlState(env), "update.json");
8295
+ var updateFile = (env = process.env) => path30.join(controlState(env), "update.json");
7943
8296
  function readUpdate(file) {
7944
8297
  try {
7945
- const j = JSON.parse(readFileSync18(file, "utf8"));
8298
+ const j = JSON.parse(readFileSync19(file, "utf8"));
7946
8299
  return j && typeof j === "object" ? j : {};
7947
8300
  } catch {
7948
8301
  return {};
@@ -7963,7 +8316,8 @@ function autoUpdateOff(o) {
7963
8316
  }
7964
8317
  function updateSummary(o) {
7965
8318
  const daemonEnv = o.rec.off?.startsWith("SESSIONPIPE_NO_UPDATE_CHECK") ? "SESSIONPIPE_NO_UPDATE_CHECK is set in the daemon's environment" : null;
7966
- const off3 = o.off ?? (o.paired ? daemonEnv : "it runs in the control daemon, which isn't set up here (`sessionpipe connect`)");
8319
+ const byHand = (o.platform ?? process.platform) === "win32" ? "it runs in the control daemon, which has no service on Windows yet: it checks while `sessionpipe control run` is running" : null;
8320
+ const off3 = o.off ?? (o.paired ? daemonEnv ?? byHand : "it runs in the control daemon, which isn't set up here (`sessionpipe connect`)");
7967
8321
  return {
7968
8322
  auto: !off3,
7969
8323
  off: off3,
@@ -8005,7 +8359,7 @@ function restartVia(o) {
8005
8359
  if (o.platform === "linux") {
8006
8360
  let cg = "";
8007
8361
  try {
8008
- cg = (o.cgroup ?? (() => readFileSync18("/proc/self/cgroup", "utf8")))();
8362
+ cg = (o.cgroup ?? (() => readFileSync19("/proc/self/cgroup", "utf8")))();
8009
8363
  } catch {
8010
8364
  }
8011
8365
  const inUnit = cg.split("\n").some((l) => l.endsWith(`/${UNIT2}`) || l.includes(`/${UNIT2}/`));
@@ -8197,14 +8551,14 @@ var AutoUpdater = class {
8197
8551
  // src/control/cli.ts
8198
8552
  import { existsSync as existsSync20, realpathSync as realpathSync7, statSync as statSync13, unwatchFile, watchFile } from "node:fs";
8199
8553
  import os14 from "node:os";
8200
- import path36 from "node:path";
8554
+ import path37 from "node:path";
8201
8555
 
8202
8556
  // src/pool/index.ts
8203
- import { randomBytes as randomBytes5 } from "node:crypto";
8557
+ import { randomBytes as randomBytes6 } from "node:crypto";
8204
8558
  import { existsSync as existsSync17 } from "node:fs";
8205
8559
  import http2 from "node:http";
8206
8560
  import net2 from "node:net";
8207
- import path32 from "node:path";
8561
+ import path33 from "node:path";
8208
8562
 
8209
8563
  // src/pool/credentials.ts
8210
8564
  var REFRESH_EARLY_MS = 5 * 6e4;
@@ -8346,14 +8700,14 @@ import {
8346
8700
  chmodSync as chmodSync6,
8347
8701
  existsSync as existsSync15,
8348
8702
  lstatSync,
8349
- mkdirSync as mkdirSync14,
8350
- readFileSync as readFileSync19,
8703
+ mkdirSync as mkdirSync13,
8704
+ readFileSync as readFileSync20,
8351
8705
  realpathSync as realpathSync4,
8352
8706
  statSync as statSync9,
8353
- unlinkSync as unlinkSync7,
8354
- writeFileSync as writeFileSync13
8707
+ unlinkSync as unlinkSync6,
8708
+ writeFileSync as writeFileSync11
8355
8709
  } from "node:fs";
8356
- import path30 from "node:path";
8710
+ import path31 from "node:path";
8357
8711
  var POOL_ENV_KEYS = [
8358
8712
  "ANTHROPIC_BASE_URL",
8359
8713
  "ANTHROPIC_AUTH_TOKEN",
@@ -8376,7 +8730,7 @@ var anySet = (env) => POOL_ENV_KEYS.some((k) => env[k] !== void 0);
8376
8730
  function readJsonFile2(file) {
8377
8731
  let raw;
8378
8732
  try {
8379
- raw = readFileSync19(file, "utf8");
8733
+ raw = readFileSync20(file, "utf8");
8380
8734
  } catch {
8381
8735
  return { ok: true, existed: false, value: {}, shape: { indent: " ", eol: "\n", finalNewline: true }, raw: "" };
8382
8736
  }
@@ -8402,7 +8756,7 @@ function formatJson2(value, f) {
8402
8756
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
8403
8757
  return f.finalNewline ? text + f.eol : text;
8404
8758
  }
8405
- var settingsIn2 = (dir) => path30.join(dir, "settings.json");
8759
+ var settingsIn2 = (dir) => path31.join(dir, "settings.json");
8406
8760
  var envOf = (settings2) => settings2.env && typeof settings2.env === "object" && !Array.isArray(settings2.env) ? settings2.env : null;
8407
8761
  var target = (file) => {
8408
8762
  try {
@@ -8422,7 +8776,7 @@ var same = (a, b) => {
8422
8776
  };
8423
8777
  return a === b || real(a) === real(b);
8424
8778
  };
8425
- var kept = (keep, file) => path30.join(keep, `${createHash8("sha256").update(file).digest("hex").slice(0, 16)}.json`);
8779
+ var kept = (keep, file) => path31.join(keep, `${createHash8("sha256").update(file).digest("hex").slice(0, 16)}.json`);
8426
8780
  var looseDir = (dir) => {
8427
8781
  try {
8428
8782
  return process.platform !== "win32" && (statSync9(dir).mode & 18) !== 0;
@@ -8460,11 +8814,11 @@ function installPoolEnv(dirs, o) {
8460
8814
  if (!changed) return { file, changed: false, ...note };
8461
8815
  settings2.env = env;
8462
8816
  if (o.keep && f.existed && !existsSync15(kept(o.keep, file))) {
8463
- mkdirSync14(o.keep, { recursive: true, mode: 448 });
8464
- writeFileSync13(kept(o.keep, file), f.raw, { mode: 384 });
8817
+ mkdirSync13(o.keep, { recursive: true, mode: 448 });
8818
+ writeFileSync11(kept(o.keep, file), f.raw, { mode: 384 });
8465
8819
  }
8466
- mkdirSync14(path30.dirname(file), { recursive: true, mode: 448 });
8467
- writeFileSync13(file, formatJson2(settings2, f.shape));
8820
+ mkdirSync13(path31.dirname(file), { recursive: true, mode: 448 });
8821
+ writeFileSync11(file, formatJson2(settings2, f.shape));
8468
8822
  try {
8469
8823
  chmodSync6(file, 384);
8470
8824
  } catch {
@@ -8485,20 +8839,20 @@ function uninstallPoolEnv(dirs, o) {
8485
8839
  for (const k of POOL_ENV_KEYS) delete env[k];
8486
8840
  if (!Object.keys(env).length) delete settings2.env;
8487
8841
  if (o.created?.some((c) => same(c, file)) && !Object.keys(settings2).length) {
8488
- unlinkSync7(file);
8842
+ unlinkSync6(file);
8489
8843
  return { file, changed: true, note: "removed (sessionpipe created it)" };
8490
8844
  }
8491
8845
  const original = o.keep ? kept(o.keep, file) : null;
8492
8846
  let text = formatJson2(settings2, f.shape);
8493
8847
  if (original && existsSync15(original)) {
8494
- const raw = readFileSync19(original, "utf8");
8848
+ const raw = readFileSync20(original, "utf8");
8495
8849
  try {
8496
8850
  if (JSON.stringify(JSON.parse(raw)) === JSON.stringify(settings2)) text = raw;
8497
8851
  } catch {
8498
8852
  }
8499
- unlinkSync7(original);
8853
+ unlinkSync6(original);
8500
8854
  }
8501
- writeFileSync13(file, text);
8855
+ writeFileSync11(file, text);
8502
8856
  return { file, changed: true };
8503
8857
  });
8504
8858
  }
@@ -9003,16 +9357,16 @@ function startPoolServer(deps) {
9003
9357
  }
9004
9358
 
9005
9359
  // src/pool/store.ts
9006
- import { existsSync as existsSync16, readFileSync as readFileSync20 } from "node:fs";
9007
- import path31 from "node:path";
9008
- var poolFile = (env = process.env) => path31.join(path31.dirname(configFile(env)), "pool.json");
9009
- var poolSecretsFile = (env = process.env) => path31.join(path31.dirname(configFile(env)), "pool.secrets.json");
9010
- var poolStateFile = (env = process.env) => path31.join(stateDir(env), "pool", "state.json");
9360
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "node:fs";
9361
+ import path32 from "node:path";
9362
+ var poolFile = (env = process.env) => path32.join(path32.dirname(configFile(env)), "pool.json");
9363
+ var poolSecretsFile = (env = process.env) => path32.join(path32.dirname(configFile(env)), "pool.secrets.json");
9364
+ var poolStateFile = (env = process.env) => path32.join(stateDir(env), "pool", "state.json");
9011
9365
  var EMPTY = { on: false, port: 0, accounts: [] };
9012
9366
  var UUID_RE5 = /^[0-9a-f-]{8,64}$/i;
9013
9367
  function readJson2(file) {
9014
9368
  try {
9015
- const j = JSON.parse(readFileSync20(file, "utf8"));
9369
+ const j = JSON.parse(readFileSync21(file, "utf8"));
9016
9370
  return j && typeof j === "object" && !Array.isArray(j) ? j : null;
9017
9371
  } catch {
9018
9372
  return null;
@@ -9270,7 +9624,7 @@ function openPool(deps) {
9270
9624
  const mint = () => {
9271
9625
  placeSecrets();
9272
9626
  const before = secrets2.get(cfg, TOKEN_NAME);
9273
- const t = randomBytes5(32).toString("base64url");
9627
+ const t = randomBytes6(32).toString("base64url");
9274
9628
  if (!secrets2.set(cfg, TOKEN_NAME, t)) throw new PoolError("pool", "the secrets store refused the pool's token");
9275
9629
  if (before && secrets2.set(cfg, PREVIOUS_TOKEN_NAME, before)) cfg.rotated_at = new Date(now()).toISOString();
9276
9630
  saveConfig();
@@ -9298,7 +9652,7 @@ function openPool(deps) {
9298
9652
  token: token ?? "",
9299
9653
  lastPort: cfg.last_port,
9300
9654
  lastToken,
9301
- keep: path32.join(stateDir(env), "pool", "settings")
9655
+ keep: path33.join(stateDir(env), "pool", "settings")
9302
9656
  });
9303
9657
  const resync = () => {
9304
9658
  const by = new Map(policy.accounts.map((a) => [a.id, a]));
@@ -9838,15 +10192,15 @@ async function runChannel(session, input, write, o) {
9838
10192
 
9839
10193
  // src/control/daemon.ts
9840
10194
  import { randomUUID } from "node:crypto";
9841
- import { chmodSync as chmodSync7, existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync22, realpathSync as realpathSync6, statSync as statSync12, unlinkSync as unlinkSync8 } from "node:fs";
10195
+ import { chmodSync as chmodSync7, existsSync as existsSync19, mkdirSync as mkdirSync15, readFileSync as readFileSync23, realpathSync as realpathSync6, statSync as statSync12, unlinkSync as unlinkSync7 } from "node:fs";
9842
10196
  import net4 from "node:net";
9843
10197
  import os13 from "node:os";
9844
- import path35 from "node:path";
10198
+ import path36 from "node:path";
9845
10199
 
9846
10200
  // src/control/files.ts
9847
10201
  import { createHash as createHash10 } from "node:crypto";
9848
- import { existsSync as existsSync18, lstatSync as lstatSync2, mkdirSync as mkdirSync15, realpathSync as realpathSync5, rmSync as rmSync2, writeFileSync as writeFileSync14 } from "node:fs";
9849
- import path33 from "node:path";
10202
+ import { existsSync as existsSync18, lstatSync as lstatSync2, mkdirSync as mkdirSync14, realpathSync as realpathSync5, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "node:fs";
10203
+ import path34 from "node:path";
9850
10204
  var FETCH_TIMEOUT_MS = 5 * 6e4;
9851
10205
  var FileProblem = class extends Error {
9852
10206
  };
@@ -9899,30 +10253,30 @@ async function fetchOne(f, o) {
9899
10253
  }
9900
10254
  var tooLong = (label, f) => new FileProblem(`${label} is larger than its signed size (${f.size} bytes)`);
9901
10255
  function realDir(p, why2) {
9902
- mkdirSync15(p, { recursive: true, mode: 448 });
10256
+ mkdirSync14(p, { recursive: true, mode: 448 });
9903
10257
  if (lstatSync2(p).isSymbolicLink()) throw new FileProblem(`${why2} is a symlink`);
9904
10258
  }
9905
10259
  function writeFiles(cwd, session, files, message) {
9906
- const top = path33.join(cwd, ".sessionpipe");
9907
- const parent = path33.join(top, "files");
10260
+ const top = path34.join(cwd, ".sessionpipe");
10261
+ const parent = path34.join(top, "files");
9908
10262
  const sub = message ? `${session}/${message}` : session;
9909
- const dir = path33.join(parent, session, ...message ? [message] : []);
10263
+ const dir = path34.join(parent, session, ...message ? [message] : []);
9910
10264
  let made = false;
9911
10265
  try {
9912
10266
  for (const part of [session, message])
9913
10267
  if (part === "." || part === "..") throw new FileProblem(`no folder ${part}`);
9914
10268
  realDir(top, ".sessionpipe");
9915
- const ignore = path33.join(top, ".gitignore");
9916
- if (!existsSync18(ignore)) writeFileSync14(ignore, "*\n", { flag: "wx", mode: 384 });
10269
+ const ignore = path34.join(top, ".gitignore");
10270
+ if (!existsSync18(ignore)) writeFileSync12(ignore, "*\n", { flag: "wx", mode: 384 });
9917
10271
  realDir(parent, ".sessionpipe/files");
9918
- if (message) realDir(path33.join(parent, session), `.sessionpipe/files/${session}`);
10272
+ if (message) realDir(path34.join(parent, session), `.sessionpipe/files/${session}`);
9919
10273
  if (existsSync18(dir)) throw new FileProblem(`.sessionpipe/files/${sub} already exists`);
9920
- mkdirSync15(dir, { mode: 448 });
10274
+ mkdirSync14(dir, { mode: 448 });
9921
10275
  made = true;
9922
10276
  if (realpathSync5.native(dir) !== dir) throw new FileProblem(".sessionpipe leads outside the session's folder");
9923
10277
  const rel = [];
9924
10278
  for (const f of files) {
9925
- writeFileSync14(path33.join(dir, f.name), f.bytes, { flag: "wx", mode: 384 });
10279
+ writeFileSync12(path34.join(dir, f.name), f.bytes, { flag: "wx", mode: 384 });
9926
10280
  rel.push(`.sessionpipe/files/${sub}/${f.name}`);
9927
10281
  }
9928
10282
  return rel;
@@ -9947,9 +10301,9 @@ ${lines.join("\n")}`;
9947
10301
 
9948
10302
  // src/control/inbox.ts
9949
10303
  import { execFileSync as execFileSync8 } from "node:child_process";
9950
- import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
10304
+ import { readFileSync as readFileSync22, statSync as statSync10 } from "node:fs";
9951
10305
  import net3 from "node:net";
9952
- import path34 from "node:path";
10306
+ import path35 from "node:path";
9953
10307
  var PEER_PROTOCOL = 1;
9954
10308
  var CONFIRM_MS = 8e3;
9955
10309
  var IDLE_STATUS = "idle";
@@ -10006,9 +10360,9 @@ function vetInbox(rec, o) {
10006
10360
  code: "not_a_window",
10007
10361
  detail: "the live process holding that session isn't one of this computer's own Claude Code windows"
10008
10362
  };
10009
- if (!rec.socket || !path34.isAbsolute(rec.socket))
10363
+ if (!rec.socket || !path35.isAbsolute(rec.socket))
10010
10364
  return { code: "no_inbox", detail: "that session's Claude Code published no local inbox" };
10011
- if (o.configDir && path34.resolve(o.configDir) !== path34.resolve(rec.configDir))
10365
+ if (o.configDir && path35.resolve(o.configDir) !== path35.resolve(rec.configDir))
10012
10366
  return {
10013
10367
  code: "other_home",
10014
10368
  detail: "the live process is signed in to a different Claude Code account from the one holding that transcript"
@@ -10085,7 +10439,7 @@ var psStart = (pid) => execFileSync8("ps", ["-o", "lstart=", "-p", String(pid)],
10085
10439
  function peerToken(rec) {
10086
10440
  if (!rec.keyFile) return null;
10087
10441
  try {
10088
- const j = JSON.parse(readFileSync21(rec.keyFile, "utf8"));
10442
+ const j = JSON.parse(readFileSync22(rec.keyFile, "utf8"));
10089
10443
  return typeof j.peerToken === "string" && TOKEN_RE.test(j.peerToken) ? j.peerToken : null;
10090
10444
  } catch {
10091
10445
  return null;
@@ -10480,7 +10834,7 @@ function startNamed(cmd) {
10480
10834
  return null;
10481
10835
  }
10482
10836
  }
10483
- var VERSION3 = true ? "0.13.0" : "0.0.0";
10837
+ var VERSION3 = true ? "0.13.2" : "0.0.0";
10484
10838
  var MAX_REPLY = 2e4;
10485
10839
  var HELLO_EVERY_MS = 10 * 6e4;
10486
10840
  var TURN_FALLBACK_MS = 30 * 6e4;
@@ -10524,7 +10878,7 @@ function askProblem(req) {
10524
10878
  if (r.from !== void 0 && !parseSessionRef(r.from)) return "from is the asking session, <harness>:<id>";
10525
10879
  if (typeof r.text !== "string" || !r.text.trim() || r.text.length > TEXT_MAX)
10526
10880
  return `an ask needs text, at most ${TEXT_MAX} characters`;
10527
- if (typeof r.cwd !== "string" || !path35.isAbsolute(r.cwd) || r.cwd.length > 1024) return "cwd is an absolute folder";
10881
+ if (typeof r.cwd !== "string" || !path36.isAbsolute(r.cwd) || r.cwd.length > 1024) return "cwd is an absolute folder";
10528
10882
  if (!Array.isArray(r.chain) || r.chain.length > ASK_CHAIN_MAX || r.chain.some((c) => !parseSessionRef(c)))
10529
10883
  return "chain is a list of sessions, <harness>:<id>, outermost first: the asks this one is nested in";
10530
10884
  if (r.permission !== void 0 && r.permission !== "read" && r.permission !== "safe")
@@ -10562,9 +10916,9 @@ var ControlDaemon = class {
10562
10916
  this.dir = dir;
10563
10917
  this.sock = sock;
10564
10918
  this.deps = deps;
10565
- this.nonces = new NonceStore(path35.join(dir, "nonces.jsonl"), deps.now);
10566
- this.acks = new AckOutbox(path35.join(dir, "acks.jsonl"));
10567
- this.state = readState(path35.join(dir, "state.json"), deps.now());
10919
+ this.nonces = new NonceStore(path36.join(dir, "nonces.jsonl"), deps.now);
10920
+ this.acks = new AckOutbox(path36.join(dir, "acks.jsonl"));
10921
+ this.state = readState(path36.join(dir, "state.json"), deps.now());
10568
10922
  this.aliases = readAliases(aliasFile(dir));
10569
10923
  }
10570
10924
  cfg;
@@ -10645,7 +10999,7 @@ var ControlDaemon = class {
10645
10999
  // --- lifecycle ------------------------------------------------------------------
10646
11000
  async start() {
10647
11001
  this.running = true;
10648
- mkdirSync16(this.dir, { recursive: true, mode: 448 });
11002
+ mkdirSync15(this.dir, { recursive: true, mode: 448 });
10649
11003
  await this.listen();
10650
11004
  await this.syncPool();
10651
11005
  for (const r of this.cfg.receivers) void this.pollLoop(r);
@@ -10688,7 +11042,7 @@ var ControlDaemon = class {
10688
11042
  this.server = null;
10689
11043
  if (process.platform !== "win32" && this.sockId && socketId(this.sock) === this.sockId)
10690
11044
  try {
10691
- unlinkSync8(this.sock);
11045
+ unlinkSync7(this.sock);
10692
11046
  } catch {
10693
11047
  }
10694
11048
  this.sockId = null;
@@ -10838,7 +11192,7 @@ var ControlDaemon = class {
10838
11192
  if (other?.op === "status") throw new DaemonRunning(other.status ?? {});
10839
11193
  if (process.platform !== "win32") {
10840
11194
  try {
10841
- if (existsSync19(this.sock)) unlinkSync8(this.sock);
11195
+ if (existsSync19(this.sock)) unlinkSync7(this.sock);
10842
11196
  } catch {
10843
11197
  }
10844
11198
  }
@@ -11479,9 +11833,9 @@ ${IN_PLACE_ANSWER}`,
11479
11833
  /** The folder the worker recorded for a session from its hooks (run.ts, facts). */
11480
11834
  factsCwd(harness, id) {
11481
11835
  try {
11482
- const f = path35.join(path35.dirname(this.dir), "facts", safeId(harness), `${safeId(id)}.json`);
11483
- const cwd = JSON.parse(readFileSync22(f, "utf8")).cwd;
11484
- return typeof cwd === "string" && path35.isAbsolute(cwd) ? cwd : void 0;
11836
+ const f = path36.join(path36.dirname(this.dir), "facts", safeId(harness), `${safeId(id)}.json`);
11837
+ const cwd = JSON.parse(readFileSync23(f, "utf8")).cwd;
11838
+ return typeof cwd === "string" && path36.isAbsolute(cwd) ? cwd : void 0;
11485
11839
  } catch {
11486
11840
  return void 0;
11487
11841
  }
@@ -11668,7 +12022,7 @@ ${IN_PLACE_ANSWER}`,
11668
12022
  * absolute, or under this user's home as `~/…` (a start's `cwd` may say either). */
11669
12023
  allowed(folder) {
11670
12024
  const dir = untilde(folder);
11671
- if (!path35.isAbsolute(dir)) return null;
12025
+ if (!path36.isAbsolute(dir)) return null;
11672
12026
  let r;
11673
12027
  try {
11674
12028
  r = realpathSync6.native(dir);
@@ -11682,7 +12036,7 @@ ${IN_PLACE_ANSWER}`,
11682
12036
  rf = realpathSync6.native(f);
11683
12037
  } catch {
11684
12038
  }
11685
- if (r === rf || r.startsWith(rf.endsWith(path35.sep) ? rf : rf + path35.sep)) return r;
12039
+ if (r === rf || r.startsWith(rf.endsWith(path36.sep) ? rf : rf + path36.sep)) return r;
11686
12040
  }
11687
12041
  return null;
11688
12042
  }
@@ -11813,7 +12167,7 @@ ${IN_PLACE_ANSWER}`,
11813
12167
  * logged and nothing else: it costs a record, never a delivery. */
11814
12168
  saveState(what) {
11815
12169
  try {
11816
- writeState(path35.join(this.dir, "state.json"), this.state);
12170
+ writeState(path36.join(this.dir, "state.json"), this.state);
11817
12171
  } catch (e) {
11818
12172
  this.deps.log(
11819
12173
  `couldn't record ${what}: ${String(
@@ -12565,7 +12919,7 @@ ${COPY_NOTE}` : "");
12565
12919
  recordStart(session, o) {
12566
12920
  this.state.started[session] = o;
12567
12921
  try {
12568
- writeState(path35.join(this.dir, "state.json"), this.state);
12922
+ writeState(path36.join(this.dir, "state.json"), this.state);
12569
12923
  } catch (e) {
12570
12924
  this.deps.log(
12571
12925
  `couldn't record what ${session.slice(0, 8)} runs with: ${String(
@@ -13043,7 +13397,7 @@ ${COPY_NOTE}` : "");
13043
13397
  );
13044
13398
  }
13045
13399
  async record(on2, type, via, data) {
13046
- const state2 = path35.dirname(this.dir);
13400
+ const state2 = path36.dirname(this.dir);
13047
13401
  const ref = { harness: on2.harness, session: on2.id };
13048
13402
  let release = takeLock(state2, ref);
13049
13403
  for (let i = 0; !release && i < 40; i++) {
@@ -13257,13 +13611,13 @@ function changeFolders(folders, op, dirs, home = os14.homedir()) {
13257
13611
  try {
13258
13612
  return realpathSync7.native(d);
13259
13613
  } catch {
13260
- return path36.resolve(d);
13614
+ return path37.resolve(d);
13261
13615
  }
13262
13616
  };
13263
13617
  const next = [...folders];
13264
13618
  const said = [];
13265
13619
  for (const d of dirs) {
13266
- const abs = path36.resolve(d);
13620
+ const abs = path37.resolve(d);
13267
13621
  const at = next.findIndex((f) => real(f) === real(abs));
13268
13622
  if (op === "remove") {
13269
13623
  if (at < 0) said.push(` \xB7 ${abs} wasn't allowed`);
@@ -13300,6 +13654,7 @@ async function controlMain(argv, out2, distDir2) {
13300
13654
  const raw = argv[2];
13301
13655
  if (!raw || raw.startsWith("-")) return out2(`usage:
13302
13656
  ${CONTROL_HELP}`);
13657
+ assertConfigWritable();
13303
13658
  if (viaNpx(distDir2)) return rerunGlobally(argv, out2);
13304
13659
  const url = (/^https?:\/\//.test(raw) ? raw : `https://${raw}`).replace(/\/$/, "");
13305
13660
  const cfg = readConfig();
@@ -13319,7 +13674,7 @@ ${CONTROL_HELP}`);
13319
13674
  );
13320
13675
  }
13321
13676
  const mode = parseMode(flag2("--mode")) ?? (existing?.mode && existing.receivers.length ? existing.mode : await askMode(out2));
13322
- const tier = argv.includes("--tier") ? Math.max(0, Math.min(3, Number(flag2("--tier")) || 0)) : 0;
13677
+ const tier = tierArg(argv, 0);
13323
13678
  await pair({
13324
13679
  url,
13325
13680
  token,
@@ -13340,7 +13695,7 @@ ${CONTROL_HELP}`);
13340
13695
  const c = readControl();
13341
13696
  out2(` Folders sessions may run in: ${c.folders.join(", ")}`);
13342
13697
  out2(` Mode: ${c.mode} (${MODE_IS[c.mode]})`);
13343
- let cli = path36.join(distDir2, "cli.js");
13698
+ let cli = path37.join(distDir2, "cli.js");
13344
13699
  try {
13345
13700
  cli = realpathSync7(cli);
13346
13701
  } catch {
@@ -13559,7 +13914,7 @@ async function runDaemon(out2, distDir2) {
13559
13914
  log2(`exiting ${UPDATE_EXIT} so ${via === "supervisor" ? "the supervising sessionpipe" : via} starts ${to}`);
13560
13915
  process.exit(UPDATE_EXIT);
13561
13916
  }
13562
- let cli = path36.join(distDir2, "cli.js");
13917
+ let cli = path37.join(distDir2, "cli.js");
13563
13918
  try {
13564
13919
  cli = realpathSync7(cli);
13565
13920
  } catch {
@@ -13642,7 +13997,7 @@ function usageTable(harnesses) {
13642
13997
  connect: "connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto|inherit] [--folder DIR]\u2026 [--status-line|--no-status-line] [--no-service]",
13643
13998
  install: `install [${harnesses.map((h) => `--${h}`).join(" ")}] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N] [--status-line|--no-status-line]`,
13644
13999
  sink: "sink add|list|remove|test (use --help after a subcommand for its options)",
13645
- "sink add": "sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--secret S] [--pii] [--name N]",
14000
+ "sink add": "sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--secret S] [--pii] [--tools all|main|beat] [--name N]",
13646
14001
  "sink list": "sink list",
13647
14002
  "sink remove": "sink remove <name>",
13648
14003
  "sink test": "sink test <name>",
@@ -13653,7 +14008,7 @@ function usageTable(harnesses) {
13653
14008
  tail: "tail [--session ID] [--tier 0-3] [--harness NAME] [--all] [--json]",
13654
14009
  backfill: `backfill [--days 30] [${harnesses.map((h) => `--${h}`).join(" ")}]`,
13655
14010
  forget: "forget <harness> <session>",
13656
- replay: "replay [--sink NAME] [--from-start]",
14011
+ replay: "replay [--sink NAME] [--from-start] [--tools all|main|beat]",
13657
14012
  update: "update [off|on] (install the latest now; off/on: the daemon's daily update)",
13658
14013
  "update off": "update off",
13659
14014
  "update on": "update on",
@@ -13705,7 +14060,7 @@ function commandHelp(args2, harnesses) {
13705
14060
 
13706
14061
  // src/pool/cli.ts
13707
14062
  import { realpathSync as realpathSync8 } from "node:fs";
13708
- import path37 from "node:path";
14063
+ import path38 from "node:path";
13709
14064
  var POOL_HELP = ` sessionpipe pool [--json] (the accounts in order, the room each has left)
13710
14065
  sessionpipe pool on | off (point every Claude Code here at the pool, or stop)
13711
14066
  sessionpipe pool add --import | --login [--code -]
@@ -13714,7 +14069,7 @@ var VENDOR_MS = 3e4;
13714
14069
  var LOCAL_MS = 5e3;
13715
14070
  var DAEMON_DOWN = " ! the daemon isn't running, so Claude Code can't reach Claude while the pool is on: `sessionpipe control run` (or `sessionpipe pool on` again to install the service)";
13716
14071
  function cliPath(distDir2) {
13717
- const cli = path37.join(distDir2, "cli.js");
14072
+ const cli = path38.join(distDir2, "cli.js");
13718
14073
  try {
13719
14074
  return realpathSync8(cli);
13720
14075
  } catch {
@@ -13913,6 +14268,32 @@ function readSecret(stdin = process.stdin, err = (s) => void process.stderr.writ
13913
14268
  });
13914
14269
  }
13915
14270
 
14271
+ // src/wellknown.ts
14272
+ var WELL_KNOWN_MAX_AGE_MS = 24 * 60 * 6e4;
14273
+ var wellKnownUrl = (sinkUrl) => `${sinkUrl.replace(/\/$/, "")}/.well-known/sessionpipe`;
14274
+ async function readWellKnown(sinkUrl, timeoutMs, fetchFn = fetch) {
14275
+ const r = await fetchFn(wellKnownUrl(sinkUrl), { signal: AbortSignal.timeout(timeoutMs) });
14276
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
14277
+ return await r.json();
14278
+ }
14279
+ function applyWellKnown(s, wk, tier, now = /* @__PURE__ */ new Date()) {
14280
+ const notes = [];
14281
+ if (typeof wk.max_tier === "number") {
14282
+ s.max_tier = wk.max_tier;
14283
+ if (wk.max_tier < tier)
14284
+ notes.push(`receiver's max_tier is ${wk.max_tier}: sending tier ${wk.max_tier}, not ${tier}`);
14285
+ }
14286
+ if (wk.endpoints) s.endpoints = wk.endpoints;
14287
+ const before = s.receiver_tools;
14288
+ if (isToolsMode(wk.tools)) s.receiver_tools = wk.tools;
14289
+ else delete s.receiver_tools;
14290
+ if (s.receiver_tools !== before && s.receiver_tools && s.receiver_tools !== "all" && !isToolsMode(s.tools))
14291
+ notes.push(`the receiver asks for tool events \`${s.receiver_tools}\` (--tools all keeps every one)`);
14292
+ s.well_known_at = now.toISOString();
14293
+ s.well_known_checked_at = s.well_known_at;
14294
+ return notes;
14295
+ }
14296
+
13916
14297
  // src/cli.ts
13917
14298
  var print = (s = "") => {
13918
14299
  process.stdout.write(`${s}
@@ -13926,7 +14307,7 @@ var flag = (name) => {
13926
14307
  return i >= 0 ? args[i + 1] : void 0;
13927
14308
  };
13928
14309
  var has = (name) => args.includes(name);
13929
- var distDir = path38.dirname(fileURLToPath(import.meta.url));
14310
+ var distDir = path39.dirname(fileURLToPath(import.meta.url));
13930
14311
  async function main(argv, io = { out: print, err: (s) => void process.stderr.write(s), exit: (c) => process.exit(c) }) {
13931
14312
  args = argv;
13932
14313
  out = io.out;
@@ -13952,7 +14333,7 @@ function hookCommand() {
13952
14333
  return (a) => ({ argv: [l, ...a], command: `${JSON.stringify(l)} ${a.join(" ")}` });
13953
14334
  }
13954
14335
  const node = stableNode(process.execPath);
13955
- let script = path38.join(distDir, "hook.js");
14336
+ let script = path39.join(distDir, "hook.js");
13956
14337
  try {
13957
14338
  script = realpathSync9(script);
13958
14339
  } catch {
@@ -13962,6 +14343,15 @@ function hookCommand() {
13962
14343
  command: `${JSON.stringify(node)} ${JSON.stringify(script)} ${a.join(" ")}`
13963
14344
  });
13964
14345
  }
14346
+ function strayWord(valued = []) {
14347
+ const word2 = args.slice(1).find((a, i) => !a.startsWith("-") && !valued.some((v) => v === args[i]));
14348
+ if (word2 === void 0) return false;
14349
+ const harness = ADAPTERS.some((a) => a.name === word2) ? `; to name that harness, pass --${word2}` : "";
14350
+ out(` ! \`sessionpipe ${args[0]}\` takes options only, and "${word2}" is not one${harness}.`);
14351
+ out(` Nothing was changed. See \`sessionpipe ${args[0]} --help\`.`);
14352
+ process.exitCode = 2;
14353
+ return true;
14354
+ }
13965
14355
  function selectedAdapters() {
13966
14356
  const named = ADAPTERS.filter((a) => has(`--${a.name}`));
13967
14357
  if (named.length) return named;
@@ -13975,6 +14365,7 @@ async function command(cmd) {
13975
14365
  );
13976
14366
  return usage ? out(usage) : help();
13977
14367
  }
14368
+ if (["connect", "install", "sink", "control", "tail"].includes(cmd)) tierArg(args, 0);
13978
14369
  switch (cmd) {
13979
14370
  case "connect":
13980
14371
  return connect();
@@ -14037,12 +14428,12 @@ Usage:
14037
14428
  (everything, in one go: hooks, a sink, signed messages, keys)
14038
14429
  sessionpipe install [--claude-code --codex --gemini-cli --antigravity --devin] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N] [--status-line|--no-status-line]
14039
14430
  sessionpipe uninstall [--claude-code --codex --gemini-cli --antigravity --devin | --all] [--keep-state]
14040
- sessionpipe sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--pii] [--name N]
14431
+ sessionpipe sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--pii] [--tools all|main|beat] [--name N]
14041
14432
  sessionpipe sink list | remove <name> | test <name>
14042
14433
  sessionpipe secrets [move keychain|secret-service|file] (where the machine's keys live)
14043
14434
  sessionpipe status [--json] | doctor [--json]
14044
14435
  sessionpipe tail [--session ID] [--tier N] [--harness NAME]
14045
- sessionpipe backfill [--days 30] | forget <harness> <session> | replay --sink NAME
14436
+ sessionpipe backfill [--days 30] | forget <harness> <session> | replay --sink NAME [--from-start] [--tools all|main|beat]
14046
14437
  sessionpipe update [off|on] (install the latest now; off/on: the daemon's daily update)
14047
14438
  sessionpipe titles [on|off|refresh] (on: name Claude Code sessions as claude.ai does; off by default)
14048
14439
  ${CONTROL_HELP}
@@ -14052,6 +14443,7 @@ ${ASK_HELP}
14052
14443
  Docs: https://sessionpipe.org \xB7 nothing leaves this machine until you add a sink.`);
14053
14444
  }
14054
14445
  function setupConfig() {
14446
+ assertConfigWritable();
14055
14447
  const cfg = readConfig();
14056
14448
  const requested = flag("--machine");
14057
14449
  if ((has("--machine") || has("--rename-machine")) && (!requested || requested.startsWith("-")))
@@ -14077,6 +14469,7 @@ function setupConfig() {
14077
14469
  return cfg;
14078
14470
  }
14079
14471
  async function install() {
14472
+ if (strayWord(["--machine", "--backfill", "--sink", "--tier", "--name", "--token", "--secret"])) return;
14080
14473
  const cfg = setupConfig();
14081
14474
  if (viaNpx(distDir)) return rerunGlobally(args, out, { allowDowngrade: has("--allow-downgrade") });
14082
14475
  assertSetupVersion(has("--allow-downgrade"));
@@ -14095,11 +14488,17 @@ async function install() {
14095
14488
  out(
14096
14489
  " No sink above tier 0: hooking one tool event per harness (the heartbeat's). `sink add \u2026 --tier 1` or higher hooks the rest."
14097
14490
  );
14098
- installHarnesses(adapters, cfg, lean);
14491
+ const skipped = installHarnesses(adapters, cfg, lean);
14099
14492
  writeConfig(cfg);
14100
14493
  const days = Number(flag("--backfill") ?? 30);
14101
14494
  if (days > 0) await backfillRows(adapters, days, cfg);
14102
14495
  out("");
14496
+ if (skipped.length === adapters.length) {
14497
+ process.exitCode = 1;
14498
+ return out(
14499
+ ` \u2717 Nothing was hooked: ${skipped.join(", ")} ${skipped.length > 1 ? "were" : "was"} left alone, for the reason above. Fix that and run \`sessionpipe install\` again.`
14500
+ );
14501
+ }
14103
14502
  const live = cfg.sinks.filter((s) => !s.paused);
14104
14503
  if (live.length) out(` Reporting to ${live.map((s) => `${s.name} (tier ${s.tier})`).join(", ")}.`);
14105
14504
  else
@@ -14141,6 +14540,7 @@ function rehook(cfg, wasLean) {
14141
14540
  writeConfig(cfg);
14142
14541
  }
14143
14542
  function installHarnesses(adapters, cfg, lean) {
14543
+ const skipped = [];
14144
14544
  if (usesLauncher()) {
14145
14545
  const node = stableNode(process.execPath);
14146
14546
  const l = installLauncher({ distDir, node, version: VERSION2 });
@@ -14176,7 +14576,9 @@ function installHarnesses(adapters, cfg, lean) {
14176
14576
  out(
14177
14577
  " note: Codex runs non-managed hooks only after you approve them: start `codex` and accept the hook review. The notify fallback reports turn ends meanwhile."
14178
14578
  );
14579
+ if (reports.every((r) => r.skipped)) skipped.push(a.name);
14179
14580
  }
14581
+ return skipped;
14180
14582
  }
14181
14583
  async function connect() {
14182
14584
  const raw = args[1];
@@ -14191,7 +14593,7 @@ async function connect() {
14191
14593
  const host = new URL(url).host;
14192
14594
  const name = machineName(cfg, os15.hostname());
14193
14595
  const before = cfg.sinks.find((x) => x.url.replace(/\/$/, "") === url);
14194
- const tier = connectTier(flag("--tier"), before?.tier);
14596
+ const tier = connectTier(has("--tier") ? tierArg(args, 0) : void 0, before?.tier);
14195
14597
  out(` Connecting ${name} to ${host}.`);
14196
14598
  const adapters = ADAPTERS.filter((a) => a.detect());
14197
14599
  if (!adapters.length)
@@ -14268,10 +14670,7 @@ async function connect() {
14268
14670
  const b = moveControlSecrets(store);
14269
14671
  const kept2 = [...a.kept, ...b.kept];
14270
14672
  out(` \u2713 Keys: in ${storeLabel(store)}${kept2.length ? ` (left in the file: ${kept2.join(", ")})` : ""}`);
14271
- } else
14272
- out(
14273
- ` Keys: in ${tilde(path38.dirname(configFile()))} at 0600 \u2014 no unlocked keychain in this session${process.env.SSH_CONNECTION ? " (an ssh shell)" : ""}`
14274
- );
14673
+ } else out(` Keys: in ${tilde(path39.dirname(configFile()))} ${keysFileNote()}`);
14275
14674
  if (rows2.length) queueBackfill(rows2, 30, readConfig());
14276
14675
  out("");
14277
14676
  out(` Done: ${name} reports to ${host} and takes messages you sign on a device it trusts.`);
@@ -14317,11 +14716,11 @@ function secrets() {
14317
14716
  return;
14318
14717
  }
14319
14718
  const cfg = readConfig();
14320
- const where = (s) => s.token_in ? `${storeLabel(s.token_in)}${sinkWithSecrets(s) ? "" : " \u2014 can't be read from this session (locked?)"}` : s.token || s.secret ? `${tilde(configFile())} (0600)` : "no key";
14719
+ const where = (s) => s.token_in ? `${storeLabel(s.token_in)}${sinkWithSecrets(s) ? "" : " \u2014 can't be read from this session (locked?)"}` : s.token || s.secret ? `${tilde(configFile())} (${fileKept()})` : "no key";
14321
14720
  for (const s of cfg.sinks) out(` sink ${s.name.padEnd(20)} ${where(s)}`);
14322
14721
  for (const r of readControl()?.receivers ?? [])
14323
14722
  out(
14324
- ` control ${new URL(r.url).host.padEnd(17)} ${r.token_in ? `${storeLabel(r.token_in)}${r.token ? "" : " \u2014 can't be read from this session (locked?)"}` : "control.json (0600)"}`
14723
+ ` control ${new URL(r.url).host.padEnd(17)} ${r.token_in ? `${storeLabel(r.token_in)}${r.token ? "" : " \u2014 can't be read from this session (locked?)"}` : `control.json (${fileKept()})`}`
14325
14724
  );
14326
14725
  if (!cfg.sinks.length && !readControl()?.receivers.length) out(" no keys on this machine");
14327
14726
  out(` best store from this session: ${storeLabel(bestStore())}`);
@@ -14341,6 +14740,7 @@ function unisolated() {
14341
14740
  }).map((a) => a.name);
14342
14741
  }
14343
14742
  async function uninstall() {
14743
+ if (strayWord()) return;
14344
14744
  const named = ADAPTERS.filter((a) => has(`--${a.name}`));
14345
14745
  const all = !named.length;
14346
14746
  if (all && !has("--all")) {
@@ -14355,13 +14755,14 @@ async function uninstall() {
14355
14755
  return;
14356
14756
  }
14357
14757
  }
14758
+ assertConfigWritable();
14358
14759
  if (all || named.some((a) => a.name === "claude-code")) await poolOffForUninstall(out);
14359
14760
  const cfg = readConfig();
14360
14761
  for (const a of all ? ADAPTERS : named) {
14361
14762
  const created = cfg.harnesses[a.name]?.created ?? [];
14362
14763
  for (const r of a.uninstall(process.env, { created }))
14363
- if (r.changed) out(` \u2713 ${a.name}: ${r.note ?? `hooks removed from ${tilde(r.file)}`}`);
14364
- else if (r.skipped) out(` ! ${a.name}: ${tilde(r.file)} \u2014 ${r.note}`);
14764
+ if (r.skipped) out(` ! ${a.name}: ${tilde(r.file)} \u2014 ${r.note}`);
14765
+ else if (r.changed) out(` \u2713 ${a.name}: ${r.note ?? `hooks removed from ${tilde(r.file)}`}`);
14365
14766
  delete cfg.harnesses[a.name];
14366
14767
  }
14367
14768
  writeConfig(cfg);
@@ -14389,7 +14790,7 @@ async function sink() {
14389
14790
  if (!cfg.sinks.length) out(" no sinks");
14390
14791
  for (const s of cfg.sinks)
14391
14792
  out(
14392
- ` ${s.name.padEnd(16)} ${s.url.padEnd(44)} tier ${s.tier}${s.max_tier !== void 0 && s.max_tier < s.tier ? ` (capped ${s.max_tier})` : ""}${s.pii ? " pii" : ""}${s.control ? " control" : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
14793
+ ` ${s.name.padEnd(16)} ${s.url.padEnd(44)} tier ${s.tier}${s.max_tier !== void 0 && s.max_tier < s.tier ? ` (capped ${s.max_tier})` : ""}${s.pii ? " pii" : ""}${s.control ? " control" : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}${toolsNote(s.tier, s.max_tier, toolsMode(s))}`
14393
14794
  );
14394
14795
  return;
14395
14796
  }
@@ -14423,16 +14824,25 @@ async function sink() {
14423
14824
  return;
14424
14825
  }
14425
14826
  out(
14426
- "usage: sessionpipe sink add <url|file:PATH|stdout> [--tier N] [--token T] [--pii] [--name N] | list | remove <name> | test <name>"
14827
+ "usage: sessionpipe sink add <url|file:PATH|stdout> [--tier N] [--token T] [--pii] [--tools all|main|beat] [--name N] | list | remove <name> | test <name>"
14427
14828
  );
14428
14829
  }
14429
14830
  async function sinkAdd(url, cfg) {
14831
+ assertConfigWritable();
14430
14832
  const tier = Math.max(0, Math.min(3, Number(flag("--tier") ?? 0)));
14431
14833
  const name = flag("--name") ?? (url === "stdout" ? "stdout" : url.startsWith("file:") ? "file" : new URL(url).host);
14432
14834
  if (has("--control"))
14433
14835
  out(" --control is ignored: control is set up per machine (`sessionpipe control pair <url>`, spec/CONTROL.md)");
14836
+ const tools = flag("--tools");
14837
+ if (tools !== void 0 && !isToolsMode(tools)) {
14838
+ out(` ! --tools takes all, main or beat (got ${JSON.stringify(tools)}): nothing was changed`);
14839
+ process.exitCode = 2;
14840
+ return;
14841
+ }
14434
14842
  const s = { name, url, tier, pii: has("--pii") };
14435
14843
  const prev = cfg.sinks.find((x) => x.name === name && x.url === url);
14844
+ const keptTools = tools ?? prev?.tools;
14845
+ if (keptTools) s.tools = keptTools;
14436
14846
  const token = flag("--token");
14437
14847
  const secret = flag("--secret");
14438
14848
  if (prev?.token_in && !token && !secret) s.token_in = prev.token_in;
@@ -14449,19 +14859,12 @@ async function sinkAdd(url, cfg) {
14449
14859
  writeConfig(cfg);
14450
14860
  out(` \u2713 sink ${name}: ${url} at tier ${Math.min(tier, s.max_tier ?? 3)}${s.pii ? ", pii" : ""}`);
14451
14861
  out(` tier ${tier} sends: ${TIER_TEXT[Math.min(tier, s.max_tier ?? 3)]}`);
14862
+ if (Math.min(tier, s.max_tier ?? 3) >= 1) out(` tool events: ${TOOLS_TEXT[toolsMode(s)]}`);
14452
14863
  }
14453
14864
  async function learnReceiver(s, tier) {
14454
14865
  if (!/^https?:\/\//.test(s.url)) return;
14455
14866
  try {
14456
- const r = await fetch(`${s.url.replace(/\/$/, "")}/.well-known/sessionpipe`, { signal: AbortSignal.timeout(8e3) });
14457
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
14458
- const wk = await r.json();
14459
- if (typeof wk.max_tier === "number") {
14460
- s.max_tier = wk.max_tier;
14461
- if (wk.max_tier < tier) out(` receiver's max_tier is ${wk.max_tier}: sending tier ${wk.max_tier}, not ${tier}`);
14462
- }
14463
- if (wk.endpoints) s.endpoints = wk.endpoints;
14464
- s.well_known_at = (/* @__PURE__ */ new Date()).toISOString();
14867
+ for (const note of applyWellKnown(s, await readWellKnown(s.url, 8e3), tier)) out(` ${note}`);
14465
14868
  } catch (e) {
14466
14869
  out(
14467
14870
  ` ! ${s.url} has no readable /.well-known/sessionpipe (${e.message}); added anyway, at tier ${tier}`
@@ -14474,6 +14877,14 @@ var TIER_TEXT = {
14474
14877
  2: "tier 1 + your prompts and the assistant's text, secrets removed",
14475
14878
  3: "everything, including tool input and output"
14476
14879
  };
14880
+ function toolsNote(tier, maxTier, mode) {
14881
+ return Math.min(tier, maxTier ?? 3) >= 1 ? ` \xB7 tools ${mode}` : "";
14882
+ }
14883
+ var TOOLS_TEXT = {
14884
+ all: "every tool call (--tools main: a subagent's at most one a minute)",
14885
+ main: "the main thread's tool calls as they happen; a subagent's at most one a minute per session (--tools all: every one)",
14886
+ beat: "at most one tool call a minute per session (--tools all: every one)"
14887
+ };
14477
14888
  function collectRows(adapters, days) {
14478
14889
  const since = Date.now() - days * 864e5;
14479
14890
  const rows2 = [];
@@ -14549,40 +14960,83 @@ async function queueBackfill(rows2, days, cfg, renames = false) {
14549
14960
  void drain(state, cfg.sinks, outbox);
14550
14961
  return { named, renamed };
14551
14962
  }
14552
- function backfill() {
14963
+ async function backfill() {
14964
+ if (strayWord(["--days"])) return;
14553
14965
  return backfillRows(selectedAdapters(), Number(flag("--days") ?? 30), readConfig());
14554
14966
  }
14967
+ async function flushLock() {
14968
+ const release = await waitNamedLock(state, "flush", 1e4);
14969
+ if (!release) {
14970
+ process.exitCode = 1;
14971
+ out(" \u2717 a drain is running (the flush lock is held); nothing was changed, try again in a moment");
14972
+ }
14973
+ return release;
14974
+ }
14975
+ async function drainIfMarked(cfg, outbox) {
14976
+ if (existsSync21(path39.join(state, "locks", "flush.more"))) await drain(state, cfg.sinks, outbox);
14977
+ }
14555
14978
  async function forget() {
14556
14979
  const [, harness, session] = args;
14557
14980
  if (!harness || !session) return out("usage: sessionpipe forget <harness> <session>");
14558
14981
  const cfg = readConfig();
14559
14982
  const outbox = new Outbox(state);
14560
14983
  const ref = { harness, session };
14561
- const e = makeEvent(
14562
- { type: "session.forgotten", harnessEvent: "forget", data: {} },
14563
- {
14564
- harness: { name: harness },
14565
- session: { id: session, seq: outbox.nextSeq(ref), machine: machineName(cfg, os15.hostname()) }
14984
+ const release = await flushLock();
14985
+ if (!release) return;
14986
+ try {
14987
+ const e = makeEvent(
14988
+ { type: "session.forgotten", harnessEvent: "forget", data: {} },
14989
+ {
14990
+ harness: { name: harness },
14991
+ session: { id: session, seq: outbox.nextSeq(ref), machine: machineName(cfg, os15.hostname()) }
14992
+ }
14993
+ );
14994
+ const sinks = buildSinks(cfg.sinks);
14995
+ for (const s of sinks) {
14996
+ const r = await s.send([filterEvent(e, s.tier, s.pii)]);
14997
+ out(r.ok ? ` \u2713 ${s.name}: asked to delete ${harness}:${session}` : ` \u2717 ${s.name}: ${r.error ?? r.status}`);
14566
14998
  }
14567
- );
14568
- const sinks = buildSinks(cfg.sinks);
14569
- for (const s of sinks) {
14570
- const r = await s.send([filterEvent(e, s.tier, s.pii)]);
14571
- out(r.ok ? ` \u2713 ${s.name}: asked to delete ${harness}:${session}` : ` \u2717 ${s.name}: ${r.error ?? r.status}`);
14999
+ outbox.delete(ref);
15000
+ out(" local outbox for the session removed, and every sink's cursor into it");
15001
+ } finally {
15002
+ release();
14572
15003
  }
14573
- outbox.delete(ref);
14574
- out(" local outbox for the session removed");
15004
+ await drainIfMarked(cfg, outbox);
14575
15005
  }
14576
15006
  async function replay() {
14577
15007
  const name = flag("--sink");
15008
+ const tools = flag("--tools");
15009
+ if (tools !== void 0 && !isToolsMode(tools)) {
15010
+ out(` ! --tools takes all, main or beat (got ${JSON.stringify(tools)}): nothing was sent`);
15011
+ process.exitCode = 2;
15012
+ return;
15013
+ }
14578
15014
  const cfg = readConfig();
14579
- const sinks = name ? cfg.sinks.filter((s) => s.name === name) : cfg.sinks;
14580
- if (!sinks.length) return out(" no such sink");
14581
- for (const s of sinks) {
14582
- if (has("--from-start")) new Cursors(state, s.name).write({});
15015
+ const chosen = name ? cfg.sinks.filter((s) => s.name === name) : cfg.sinks;
15016
+ if (!chosen.length) return out(" no such sink");
15017
+ const sinks = tools ? chosen.map((s) => ({ ...s, tools })) : chosen;
15018
+ const release = await flushLock();
15019
+ if (!release) return;
15020
+ const outbox = new Outbox(state);
15021
+ try {
15022
+ for (const s of sinks) {
15023
+ if (has("--from-start")) {
15024
+ new Cursors(state, s.name).write({});
15025
+ new ToolClock(state, s.name).write({});
15026
+ }
15027
+ }
15028
+ const thinned = {};
15029
+ const d = await flush(state, sinks, outbox, void 0, { force: true, thinned });
15030
+ for (const [k, v] of Object.entries(d)) {
15031
+ const t = thinned[k] ?? 0;
15032
+ out(
15033
+ ` ${k}: ${v} event${v === 1 ? "" : "s"} delivered${t ? `, ${t} tool event${t === 1 ? "" : "s"} thinned (--tools all sends every one)` : ""}`
15034
+ );
15035
+ }
15036
+ } finally {
15037
+ release();
14583
15038
  }
14584
- const d = await flush(state, sinks, new Outbox(state), void 0, { force: true });
14585
- for (const [k, v] of Object.entries(d)) out(` ${k}: ${v} event${v === 1 ? "" : "s"} delivered`);
15039
+ await drainIfMarked(cfg, outbox);
14586
15040
  }
14587
15041
  async function controlHealth() {
14588
15042
  if (!pairedCount()) return null;
@@ -14657,13 +15111,14 @@ async function status2() {
14657
15111
  tier: s.tier,
14658
15112
  max_tier: s.max_tier ?? null,
14659
15113
  pii: !!s.pii,
15114
+ tools: toolsMode(s),
14660
15115
  control: !!s.control,
14661
15116
  paused: s.paused ?? null,
14662
15117
  pending_sessions: pending
14663
15118
  });
14664
15119
  if (!json2)
14665
15120
  out(
14666
- ` sink ${s.name.padEnd(16)} tier ${Math.min(s.tier, s.max_tier ?? 3)} ${s.paused ? `PAUSED ${s.paused}` : pending ? `${pending} session file(s) with undelivered events` : "up to date"}`
15121
+ ` sink ${s.name.padEnd(16)} tier ${Math.min(s.tier, s.max_tier ?? 3)} ${s.paused ? `PAUSED ${s.paused}` : pending ? `${pending} session file(s) with undelivered events` : "up to date"}${toolsNote(s.tier, s.max_tier, toolsMode(s))}`
14667
15122
  );
14668
15123
  }
14669
15124
  const control = await controlHealth();
@@ -14709,12 +15164,12 @@ async function status2() {
14709
15164
  else if (jobs) out(` jobs: ${jobs} waiting`);
14710
15165
  }
14711
15166
  function hookWall(runs = 8) {
14712
- const hook = path38.join(distDir, "hook.js");
14713
- const tmp = mkdtempSync(path38.join(os15.tmpdir(), "sessionpipe-doctor-"));
15167
+ const hook = path39.join(distDir, "hook.js");
15168
+ const tmp = mkdtempSync(path39.join(os15.tmpdir(), "sessionpipe-doctor-"));
14714
15169
  const env = {
14715
15170
  ...process.env,
14716
15171
  SESSIONPIPE_STATE: tmp,
14717
- SESSIONPIPE_CONFIG: path38.join(tmp, "config.json"),
15172
+ SESSIONPIPE_CONFIG: path39.join(tmp, "config.json"),
14718
15173
  SESSIONPIPE_NO_WORKER: "1"
14719
15174
  };
14720
15175
  const input = JSON.stringify({
@@ -14745,7 +15200,7 @@ function hookWall(runs = 8) {
14745
15200
  }
14746
15201
  function timing() {
14747
15202
  try {
14748
- const lines = readFileSync23(path38.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
15203
+ const lines = readFileSync24(path39.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
14749
15204
  const ms2 = lines.map((l) => JSON.parse(l).ms).filter((x) => x >= 0).sort((a, b) => a - b);
14750
15205
  if (!ms2.length) return null;
14751
15206
  const q = (p) => Math.round(
@@ -14874,6 +15329,7 @@ async function doctor() {
14874
15329
  tier: s.tier,
14875
15330
  max_tier: s.max_tier ?? null,
14876
15331
  pii: !!s.pii,
15332
+ tools: toolsMode(s),
14877
15333
  control: !!s.control,
14878
15334
  paused: s.paused ?? null,
14879
15335
  token: s.token_in ? `in ${s.token_in}` : s.token ? `${s.token.slice(0, 4)}\u2026` : null
@@ -14893,7 +15349,7 @@ async function doctor() {
14893
15349
  for (const r of st) out(` ${h.padEnd(12)} ${r.file.padEnd(44)} ${r.state}`);
14894
15350
  for (const s of report.sinks)
14895
15351
  out(
14896
- ` sink ${s.name.padEnd(16)} tier ${s.tier}${s.max_tier !== null ? ` (receiver max ${s.max_tier})` : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
15352
+ ` sink ${s.name.padEnd(16)} tier ${s.tier}${s.max_tier !== null ? ` (receiver max ${s.max_tier})` : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}${toolsNote(s.tier, s.max_tier, s.tools)}`
14897
15353
  );
14898
15354
  controlLines(health);
14899
15355
  const lr = report.launcher;
@@ -14917,7 +15373,7 @@ async function doctor() {
14917
15373
  if (!warnings.length) out(" no warnings");
14918
15374
  }
14919
15375
  async function tail() {
14920
- const tier = Math.max(0, Math.min(3, Number(flag("--tier") ?? 1)));
15376
+ const tier = tierArg(args, 1);
14921
15377
  const onlySession = flag("--session");
14922
15378
  const onlyHarness = flag("--harness");
14923
15379
  const outbox = new Outbox(state);
@@ -14966,7 +15422,7 @@ async function tail() {
14966
15422
  }
14967
15423
  return `${t} ${e.harness.name.padEnd(11)} ${e.session.id.slice(0, 4)}\u2026 ${e.type.padEnd(17)} ${detail}`;
14968
15424
  };
14969
- out(`sessionpipe tail \xB7 tier ${tier} \xB7 ${tilde(path38.join(state, "outbox"))} (Ctrl-C to stop)`);
15425
+ out(`sessionpipe tail \xB7 tier ${tier} \xB7 ${tilde(path39.join(state, "outbox"))} (Ctrl-C to stop)`);
14970
15426
  for (const ref of outbox.sessions()) {
14971
15427
  try {
14972
15428
  offsets.set(`${ref.harness}/${ref.session}`, has("--all") ? 0 : statSync14(outbox.file(ref)).size);
@@ -14992,9 +15448,9 @@ async function tail() {
14992
15448
  async function workerFromArgv() {
14993
15449
  const f = args[1];
14994
15450
  if (!f) return;
14995
- const job = JSON.parse(readFileSync23(f, "utf8"));
15451
+ const job = JSON.parse(readFileSync24(f, "utf8"));
14996
15452
  try {
14997
- unlinkSync9(f);
15453
+ unlinkSync8(f);
14998
15454
  } catch {
14999
15455
  }
15000
15456
  await runJob(job);
@@ -15018,6 +15474,10 @@ async function update() {
15018
15474
  sub === "off" ? " \u2713 The control daemon no longer checks npm or updates itself. `sessionpipe update` still updates by hand; `sessionpipe update on` undoes this." : " \u2713 The control daemon checks npm once a day and installs a new release when it's idle."
15019
15475
  );
15020
15476
  }
15477
+ if (sub !== void 0) {
15478
+ process.exitCode = 2;
15479
+ return out("usage: sessionpipe update [off|on]");
15480
+ }
15021
15481
  if (process.env.SESSIONPIPE_NO_UPDATE_CHECK) return out(" update check is off (SESSIONPIPE_NO_UPDATE_CHECK)");
15022
15482
  let latest;
15023
15483
  try {