sessionpipe 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +61 -7
  2. package/dist/ask.d.ts +40 -0
  3. package/dist/ask.d.ts.map +1 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +4439 -2047
  6. package/dist/cli.js.map +4 -4
  7. package/dist/connect.d.ts +6 -1
  8. package/dist/connect.d.ts.map +1 -1
  9. package/dist/control/autoupdate.d.ts +1 -0
  10. package/dist/control/autoupdate.d.ts.map +1 -1
  11. package/dist/control/channel.d.ts +43 -0
  12. package/dist/control/channel.d.ts.map +1 -0
  13. package/dist/control/cli.d.ts +13 -1
  14. package/dist/control/cli.d.ts.map +1 -1
  15. package/dist/control/daemon.d.ts +189 -2
  16. package/dist/control/daemon.d.ts.map +1 -1
  17. package/dist/control/inbox.d.ts +119 -0
  18. package/dist/control/inbox.d.ts.map +1 -0
  19. package/dist/control/local.d.ts +69 -4
  20. package/dist/control/local.d.ts.map +1 -1
  21. package/dist/control/pair.d.ts.map +1 -1
  22. package/dist/control/reply.d.ts +25 -1
  23. package/dist/control/reply.d.ts.map +1 -1
  24. package/dist/control/route.d.ts +50 -2
  25. package/dist/control/route.d.ts.map +1 -1
  26. package/dist/control/sdk.d.ts.map +1 -1
  27. package/dist/control/store.d.ts +49 -4
  28. package/dist/control/store.d.ts.map +1 -1
  29. package/dist/fold.d.ts +29 -0
  30. package/dist/fold.d.ts.map +1 -0
  31. package/dist/help.d.ts.map +1 -1
  32. package/dist/hook.d.ts.map +1 -1
  33. package/dist/hook.js +15 -5
  34. package/dist/hook.js.map +2 -2
  35. package/dist/index.js +1272 -257
  36. package/dist/index.js.map +4 -4
  37. package/dist/run.d.ts +41 -5
  38. package/dist/run.d.ts.map +1 -1
  39. package/dist/secrets.d.ts +14 -4
  40. package/dist/secrets.d.ts.map +1 -1
  41. package/dist/wellknown.d.ts +31 -0
  42. package/dist/wellknown.d.ts.map +1 -0
  43. package/dist/worker.d.ts +7 -1
  44. package/dist/worker.d.ts.map +1 -1
  45. package/dist/worker.js +1372 -279
  46. package/dist/worker.js.map +4 -4
  47. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
- // sessionpipe 0.12.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.13.1 · Apache-2.0 · https://sessionpipe.org
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
2
7
 
3
8
  // src/http-sink.ts
4
9
  import { createHash, createHmac } from "node:crypto";
@@ -99,16 +104,16 @@ var HttpSink = class {
99
104
  import {
100
105
  appendFileSync as appendFileSync3,
101
106
  existsSync as existsSync10,
102
- mkdirSync as mkdirSync8,
103
- readdirSync as readdirSync6,
104
- readFileSync as readFileSync10,
105
- renameSync as renameSync3,
106
- statSync as statSync6,
107
- unlinkSync as unlinkSync4,
108
- writeFileSync as writeFileSync8
107
+ mkdirSync as mkdirSync7,
108
+ readdirSync as readdirSync7,
109
+ readFileSync as readFileSync12,
110
+ renameSync as renameSync4,
111
+ statSync as statSync7,
112
+ unlinkSync as unlinkSync3,
113
+ writeFileSync as writeFileSync6
109
114
  } from "node:fs";
110
- import os3 from "node:os";
111
- import path15 from "node:path";
115
+ import os4 from "node:os";
116
+ import path17 from "node:path";
112
117
 
113
118
  // ../core/src/paths.ts
114
119
  import os from "node:os";
@@ -136,7 +141,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
136
141
  // ../core/src/adapters/claude-code.ts
137
142
  import { execFileSync as execFileSync2 } from "node:child_process";
138
143
  import { createHash as createHash2 } from "node:crypto";
139
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
144
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
140
145
  import { userInfo } from "node:os";
141
146
  import path4 from "node:path";
142
147
 
@@ -288,7 +293,14 @@ ${m.text}`;
288
293
  continue;
289
294
  }
290
295
  if (cur) turns.push(cur);
291
- cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
296
+ cur = {
297
+ user: m.text,
298
+ assistant: "",
299
+ at: m.at,
300
+ startLine: m.line,
301
+ endLine: m.line,
302
+ ...m.injected ? { injected: true } : {}
303
+ };
292
304
  } else {
293
305
  if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
294
306
  cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
@@ -298,10 +310,17 @@ ${m.text}`;
298
310
  if (cur?.assistant) turns.push(cur);
299
311
  const line = cur && !cur.assistant ? cur.startLine : endLine;
300
312
  const out = {
301
- turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
313
+ turns: turns.map((t) => ({
314
+ user: t.user,
315
+ assistant: t.assistant,
316
+ at: t.at,
317
+ endLine: t.endLine,
318
+ ...t.injected ? { injected: true } : {}
319
+ })),
302
320
  line
303
321
  };
304
- if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
322
+ if (cur && !cur.assistant && cur.user)
323
+ out.open = { user: cur.user, at: cur.at, ...cur.injected ? { injected: true } : {} };
305
324
  return out;
306
325
  }
307
326
  var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
@@ -324,27 +343,51 @@ var textParts = (c, kinds) => {
324
343
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
325
344
  ).map((b) => b.text).join("\n");
326
345
  };
346
+ var SPOKEN = /* @__PURE__ */ new Set(["human", "peer"]);
347
+ var PEER_INTRO = /sent a message[^\n]*:$/;
348
+ var PEER_OUTRO = "\n\nThis came from another ";
349
+ var ENVELOPE = /^<cross-session-message\b[^>]*>\n([\s\S]*)\n<\/cross-session-message>$/;
350
+ function peerAsk(origin, text) {
351
+ const body = origin?.body;
352
+ let t = typeof body === "string" && body.trim() ? body : text;
353
+ if (t !== body) {
354
+ const nl = t.indexOf("\n");
355
+ if (nl > 0 && PEER_INTRO.test(t.slice(0, nl))) t = t.slice(nl + 1);
356
+ const outro = t.indexOf(PEER_OUTRO);
357
+ if (outro > 0) t = t.slice(0, outro);
358
+ }
359
+ return (ENVELOPE.exec(t.trim())?.[1] ?? t).trim();
360
+ }
327
361
  var queuedPrompt = (a) => {
328
362
  if (!a || typeof a !== "object") return "";
329
363
  const q = a;
330
364
  if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
331
- if (q.origin?.kind && q.origin.kind !== "human") return "";
332
- return textParts(q.prompt, ["text"]).trim();
365
+ const kind = q.origin?.kind ?? "human";
366
+ if (!SPOKEN.has(kind)) return "";
367
+ const text = textParts(q.prompt, ["text"]).trim();
368
+ return kind === "peer" ? peerAsk(q.origin, text) : text;
333
369
  };
370
+ var channelText = (text) => /^<channel\b[^>]*>([\s\S]*)<\/channel>$/.exec(text)?.[1]?.trim() ?? "";
334
371
  function readClaude(lines, from) {
335
372
  const msgs = [];
336
373
  for (let i = from; i < lines.length; i++) {
337
374
  const j = lines[i] ? parse(lines[i]) : null;
338
375
  if (!j || j.isSidechain) continue;
339
376
  const origin = j.origin;
340
- if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
341
- continue;
377
+ const channel = j.type === "user" && origin?.kind === "channel";
378
+ const peer = origin?.kind === "peer";
379
+ if (j.type === "user" && (j.isCompactSummary || j.repeatsCompactSummary)) continue;
380
+ if (j.type === "user" && !channel && !peer && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
342
381
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
343
382
  const c = j.message?.content;
344
- if (j.type === "user") {
345
- const text = textParts(c, ["text"]).trim();
383
+ if (channel) {
384
+ const text = channelText(textParts(c, ["text"]).trim());
385
+ if (text) msgs.push({ role: "user", text, at, line: i, injected: true });
386
+ } else if (j.type === "user") {
387
+ const raw = textParts(c, ["text"]).trim();
388
+ const text = peer ? peerAsk(origin, raw) : raw;
346
389
  if (!text || isInjected(text)) continue;
347
- msgs.push({ role: "user", text, at, line: i });
390
+ msgs.push({ role: "user", text, at, line: i, ...peer ? { injected: true } : {} });
348
391
  } else if (j.type === "assistant" && Array.isArray(c)) {
349
392
  const text = textParts(c, ["text"]).trim();
350
393
  if (text) msgs.push({ role: "assistant", text, at, line: i });
@@ -456,6 +499,41 @@ function readJson(file) {
456
499
  const f = readJsonFile(file);
457
500
  return f.ok ? f.value : {};
458
501
  }
502
+ function refused(file, e, what, existed) {
503
+ const { code, syscall } = e;
504
+ const why = `can't ${what} it (${code ?? String(e)})`;
505
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
506
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
507
+ return {
508
+ file,
509
+ changed: true,
510
+ skipped: true,
511
+ ...existed ? {} : { created: true },
512
+ note: `${why} part way; check the file before the harness reads it`
513
+ };
514
+ }
515
+ function writeBack(file, text, existed = true) {
516
+ try {
517
+ mkdirSync(path3.dirname(file), { recursive: true });
518
+ writeFileSync(file, text);
519
+ return null;
520
+ } catch (e) {
521
+ return refused(file, e, "write", existed);
522
+ }
523
+ }
524
+ function removeEmptied(file, empty) {
525
+ try {
526
+ unlinkSync(file);
527
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
528
+ } catch (e) {
529
+ const bad = refused(file, e, "remove", true);
530
+ return writeBack(file, empty) ?? {
531
+ file,
532
+ changed: true,
533
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
534
+ };
535
+ }
536
+ }
459
537
  function entryFor(event, harness, cmd, o) {
460
538
  const matcher = o.matcherFor(event);
461
539
  return {
@@ -497,8 +575,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
497
575
  }
498
576
  if (changed) {
499
577
  settings2.hooks = hooks;
500
- mkdirSync(path3.dirname(file), { recursive: true });
501
- writeFileSync(file, formatJson(settings2, f));
578
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
579
+ if (bad) return bad;
502
580
  }
503
581
  return { file, changed, created: !f.existed && changed };
504
582
  }
@@ -521,12 +599,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
521
599
  }
522
600
  if (!changed) return { file, changed: false };
523
601
  if (!Object.keys(hooks).length) delete settings2.hooks;
524
- if (opts.created && !Object.keys(settings2).length) {
525
- unlinkSync(file);
526
- return { file, changed: true, note: "removed (sessionpipe created it)" };
527
- }
528
- writeFileSync(file, formatJson(settings2, f));
529
- return { file, changed: true };
602
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
603
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
530
604
  }
531
605
  function installedClaudeShaped(file, harness, events, cmd, o) {
532
606
  const f = readJsonFile(file);
@@ -605,7 +679,7 @@ var claudeCode = {
605
679
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
606
680
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
607
681
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
608
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
682
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
609
683
  return line ? [hooks, line] : [hooks];
610
684
  }),
611
685
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -613,7 +687,9 @@ var claudeCode = {
613
687
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
614
688
  created: opts.created?.includes(settingsIn(d))
615
689
  });
616
- return line && !hooks.changed ? line : hooks;
690
+ if (line?.changed && !line.skipped && hooks.skipped)
691
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
692
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
617
693
  }),
618
694
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
619
695
  file: settingsIn(d),
@@ -708,7 +784,8 @@ var claudeCode = {
708
784
  attention_id: call_id ?? permissionId(),
709
785
  kind: "permission",
710
786
  tool,
711
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
787
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
788
+ agent_id
712
789
  })
713
790
  );
714
791
  break;
@@ -717,7 +794,8 @@ var claudeCode = {
717
794
  ev("attention.needed", {
718
795
  attention_id: `notif-${Date.now()}`,
719
796
  kind: notifKind(s.notification_type),
720
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
797
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
798
+ agent_id
721
799
  })
722
800
  );
723
801
  break;
@@ -842,8 +920,11 @@ function statusLine(file, want, cmd) {
842
920
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
843
921
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
844
922
  else delete f.value.statusLine;
845
- writeFileSync2(file, formatJson(f.value, f));
846
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
923
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
924
+ file,
925
+ changed: true,
926
+ note: want ? "status line written (account limits)" : "status line removed"
927
+ };
847
928
  }
848
929
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
849
930
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1258,8 +1339,44 @@ async function claudeAiTitle(bridge, token) {
1258
1339
  }
1259
1340
 
1260
1341
  // ../core/src/adapters/headless.ts
1342
+ import { spawn } from "node:child_process";
1261
1343
  import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1262
1344
  import path5 from "node:path";
1345
+
1346
+ // ../core/src/launched-by.ts
1347
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
1348
+ var DELEGATED_BY_ENV = "SESSIONPIPE_ASKED_BY";
1349
+ var ASK_CHAIN_ENV = "SESSIONPIPE_ASK_CHAIN";
1350
+ var LINEAGE_ENV = [LAUNCHED_BY_ENV, DELEGATED_BY_ENV, ASK_CHAIN_ENV];
1351
+ var SESSION_REF = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
1352
+ function withoutLineage(env) {
1353
+ const e = { ...env };
1354
+ for (const k of LINEAGE_ENV) delete e[k];
1355
+ return e;
1356
+ }
1357
+ function refIn(env, name) {
1358
+ const v = env[name]?.trim();
1359
+ return v && SESSION_REF.test(v) ? v : void 0;
1360
+ }
1361
+ function launchedBy(env) {
1362
+ return refIn(env, LAUNCHED_BY_ENV);
1363
+ }
1364
+ function delegatedBy(env) {
1365
+ return refIn(env, DELEGATED_BY_ENV);
1366
+ }
1367
+ function readHook(adapter, input) {
1368
+ const r = adapter.fromHook(input);
1369
+ if (r && !r.session.parent_id) {
1370
+ const self = `${adapter.name}:${r.session.id}`;
1371
+ const launcher = launchedBy(input.env);
1372
+ if (launcher && launcher !== self) r.session.launched_by = launcher;
1373
+ const asker = delegatedBy(input.env);
1374
+ if (asker && asker !== self) r.session.delegated_by = asker;
1375
+ }
1376
+ return r;
1377
+ }
1378
+
1379
+ // ../core/src/adapters/headless.ts
1263
1380
  var JOB_TIMEOUT_MS = 30 * 6e4;
1264
1381
  var OUT_CAP = 1024 * 1024;
1265
1382
  function isExe(p) {
@@ -1301,22 +1418,119 @@ function spawnable(bin, args) {
1301
1418
  const target = shimTarget(bin);
1302
1419
  return target ? { file: process.execPath, args: [target, ...args] } : null;
1303
1420
  }
1421
+ var shimNote = (bin) => `${path5.basename(bin)} is a launcher script that names no program to run; point the harness's override (SESSIONPIPE_CLAUDE, SESSIONPIPE_CODEX, \u2026) at the program itself`;
1304
1422
  function probe(bin, args) {
1305
1423
  const s = spawnable(bin, args);
1306
1424
  return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1307
1425
  }
1426
+ function runHeadless(bin, args, o) {
1427
+ return new Promise((resolve) => {
1428
+ let out = [];
1429
+ let outLen = 0;
1430
+ let tooBig = false;
1431
+ let errTail = "";
1432
+ let timedOut = false;
1433
+ let settled = false;
1434
+ let child;
1435
+ const c = spawnable(bin, args);
1436
+ if (!c) {
1437
+ resolve({ error: shimNote(bin) });
1438
+ return;
1439
+ }
1440
+ try {
1441
+ child = spawn(c.file, c.args, {
1442
+ cwd: o.cwd,
1443
+ env: o.env,
1444
+ stdio: ["ignore", "pipe", "pipe"],
1445
+ detached: process.platform !== "win32",
1446
+ windowsHide: true
1447
+ });
1448
+ } catch (e) {
1449
+ resolve({ error: e.message });
1450
+ return;
1451
+ }
1452
+ const kill = (sig) => {
1453
+ try {
1454
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, sig);
1455
+ else child.kill(sig);
1456
+ } catch {
1457
+ try {
1458
+ child.kill(sig);
1459
+ } catch {
1460
+ }
1461
+ }
1462
+ };
1463
+ const keep = (line) => {
1464
+ if (!o.keep?.(line)) return;
1465
+ out = [Buffer.from(line)];
1466
+ outLen = line.length;
1467
+ };
1468
+ let partial = "";
1469
+ child.stdout?.on("data", (b) => {
1470
+ if (o.onLine) {
1471
+ const chunk = partial + b.toString("utf8");
1472
+ const lines = chunk.split("\n");
1473
+ partial = lines.pop();
1474
+ for (const line of lines) {
1475
+ if (!line.trim()) continue;
1476
+ try {
1477
+ o.onLine(line);
1478
+ } catch {
1479
+ }
1480
+ keep(line);
1481
+ }
1482
+ if (partial.length > OUT_CAP) partial = "";
1483
+ return;
1484
+ }
1485
+ if (tooBig) return;
1486
+ if (outLen + b.length > OUT_CAP) {
1487
+ tooBig = true;
1488
+ out = [];
1489
+ return;
1490
+ }
1491
+ out.push(b);
1492
+ outLen += b.length;
1493
+ });
1494
+ child.stderr?.on("data", (b) => {
1495
+ errTail = (errTail + b.toString("utf8")).slice(-4e3);
1496
+ });
1497
+ const timer = setTimeout(() => {
1498
+ timedOut = true;
1499
+ kill("SIGTERM");
1500
+ setTimeout(() => kill("SIGKILL"), 1e4).unref();
1501
+ }, o.timeoutMs ?? JOB_TIMEOUT_MS);
1502
+ const result = (code, signal) => ({
1503
+ code,
1504
+ signal,
1505
+ stdout: tooBig ? null : Buffer.concat(out).toString("utf8"),
1506
+ tooBig,
1507
+ stderr: errTail,
1508
+ timedOut
1509
+ });
1510
+ const finish = (r) => {
1511
+ if (settled) return;
1512
+ settled = true;
1513
+ clearTimeout(timer);
1514
+ resolve(r);
1515
+ };
1516
+ child.on("error", (e) => finish({ error: e.message }));
1517
+ child.on("exit", (code, signal) => setTimeout(() => finish(result(code, signal)), 2e3).unref());
1518
+ child.on("close", (code, signal) => {
1519
+ if (o.onLine && partial.trim()) {
1520
+ try {
1521
+ o.onLine(partial);
1522
+ } catch {
1523
+ }
1524
+ keep(partial);
1525
+ partial = "";
1526
+ }
1527
+ finish(result(code, signal));
1528
+ });
1529
+ });
1530
+ }
1308
1531
 
1309
1532
  // ../core/src/adapters/antigravity.ts
1310
- import {
1311
- existsSync as existsSync3,
1312
- mkdirSync as mkdirSync2,
1313
- readdirSync as readdirSync3,
1314
- readFileSync as readFileSync4,
1315
- realpathSync,
1316
- statSync as statSync4,
1317
- unlinkSync as unlinkSync2,
1318
- writeFileSync as writeFileSync3
1319
- } from "node:fs";
1533
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync4 } from "node:fs";
1320
1534
  import path6 from "node:path";
1321
1535
  var NAME2 = "antigravity";
1322
1536
  var HOOK_NAME = "sessionpipe";
@@ -1348,10 +1562,8 @@ var antigravity = {
1348
1562
  const e = entry(cmd);
1349
1563
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1350
1564
  hooks[HOOK_NAME] = e;
1351
- mkdirSync2(path6.dirname(file), { recursive: true });
1352
- writeFileSync3(file, formatJson(hooks, f));
1353
1565
  return [
1354
- {
1566
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
1355
1567
  file,
1356
1568
  changed: true,
1357
1569
  created: !f.existed,
@@ -1367,12 +1579,8 @@ var antigravity = {
1367
1579
  const hooks = f.value;
1368
1580
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1369
1581
  delete hooks[HOOK_NAME];
1370
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1371
- unlinkSync2(file);
1372
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1373
- }
1374
- writeFileSync3(file, formatJson(hooks, f));
1375
- return [{ file, changed: true }];
1582
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
1583
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
1376
1584
  },
1377
1585
  installed(cmd, env = process.env) {
1378
1586
  const file = hooksFile(env);
@@ -1594,6 +1802,50 @@ function antigravityFacts(file, opts) {
1594
1802
  return out;
1595
1803
  }
1596
1804
 
1805
+ // ../core/src/adapters/claude-code-control.ts
1806
+ var claude_code_control_exports = {};
1807
+ __export(claude_code_control_exports, {
1808
+ CLAUDE_MODES: () => CLAUDE_MODES,
1809
+ JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
1810
+ MODE_RANK: () => MODE_RANK,
1811
+ NO_HOME_SETTINGS: () => NO_HOME_SETTINGS,
1812
+ ceilingLevel: () => ceilingLevel,
1813
+ claudeArgs: () => claudeArgs,
1814
+ claudeEnv: () => claudeEnv,
1815
+ claudeLogin: () => claudeLogin2,
1816
+ cmpVersion: () => cmpVersion,
1817
+ defaultLevel: () => defaultLevel,
1818
+ detectCaps: () => detectCaps,
1819
+ findClaude: () => findClaude,
1820
+ findTranscript: () => findTranscript,
1821
+ findTranscripts: () => findTranscripts,
1822
+ homeSettings: () => homeSettings,
1823
+ hookMode: () => hookMode,
1824
+ isUuid: () => isUuid,
1825
+ liveProcess: () => liveProcess,
1826
+ liveRecord: () => liveRecord,
1827
+ liveRecords: () => liveRecords,
1828
+ loginCommand: () => loginCommand,
1829
+ loginHint: () => loginHint,
1830
+ modeFlags: () => modeFlags,
1831
+ modeRank: () => modeRank,
1832
+ offeredLevels: () => offeredLevels,
1833
+ outputArgs: () => outputArgs,
1834
+ parseAuthStatus: () => parseAuthStatus,
1835
+ parseCaps: () => parseCaps,
1836
+ parseResult: () => parseResult,
1837
+ peerClass: () => peerClass,
1838
+ recentlyWritten: () => recentlyWritten,
1839
+ relaunchMode: () => relaunchMode,
1840
+ resolveHome: () => resolveHome,
1841
+ runClaude: () => runClaude,
1842
+ sessionFolder: () => sessionFolder
1843
+ });
1844
+ import { execFile, execFileSync as execFileSync3 } from "node:child_process";
1845
+ import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, readSync as readSync2, statSync as statSync5 } from "node:fs";
1846
+ import os2 from "node:os";
1847
+ import path7 from "node:path";
1848
+
1597
1849
  // ../core/src/control/verify.ts
1598
1850
  var GRANT_MAX_MS = 24 * 36e5;
1599
1851
  var SKEW_MS = 5 * 6e4;
@@ -1604,10 +1856,10 @@ var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1604
1856
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1605
1857
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1606
1858
  var FIELDS = {
1607
- prompt: ["text", "files"],
1859
+ prompt: ["text", "files", "reply_to"],
1608
1860
  "permission.answer": ["for", "decision"],
1609
1861
  cancel: [],
1610
- start: ["text", "cwd", "files", "permission", "model"],
1862
+ start: ["text", "cwd", "files", "permission", "model", "reply_to"],
1611
1863
  "key.add": ["key", "name"],
1612
1864
  pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
1613
1865
  };
@@ -1615,9 +1867,454 @@ var POOL_FIELDS = FIELDS.pool;
1615
1867
  var enc = new TextEncoder();
1616
1868
  var dec = new TextDecoder();
1617
1869
 
1870
+ // ../core/src/adapters/claude-code-control.ts
1871
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1872
+ var LIVE_WRITE_MS = 9e4;
1873
+ var cmpVersion = (a, b) => {
1874
+ const pa = a.split(".").map(Number);
1875
+ const pb = b.split(".").map(Number);
1876
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++)
1877
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
1878
+ return 0;
1879
+ };
1880
+ function findClaude(env = process.env, home = os2.homedir()) {
1881
+ if (env.SESSIONPIPE_CLAUDE && isExe(env.SESSIONPIPE_CLAUDE)) return env.SESSIONPIPE_CLAUDE;
1882
+ const names = process.platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
1883
+ for (const d of String(env.PATH || "").split(path7.delimiter)) {
1884
+ if (!d || !path7.isAbsolute(d)) continue;
1885
+ for (const n of names) if (isExe(path7.join(d, n))) return path7.join(d, n);
1886
+ }
1887
+ for (const p of [
1888
+ path7.join(home, ".claude", "local", "claude"),
1889
+ path7.join(home, ".local", "bin", "claude"),
1890
+ "/opt/homebrew/bin/claude",
1891
+ "/usr/local/bin/claude"
1892
+ ])
1893
+ if (isExe(p)) return p;
1894
+ const app = path7.join(home, "Library", "Application Support", "Claude", "claude-code");
1895
+ let versions = [];
1896
+ try {
1897
+ versions = readdirSync4(app).filter((n) => /^\d+(\.\d+)*$/.test(n)).sort(cmpVersion).reverse();
1898
+ } catch {
1899
+ }
1900
+ for (const v of versions) {
1901
+ const p = path7.join(app, v, "claude.app", "Contents", "MacOS", "claude");
1902
+ if (isExe(p)) return p;
1903
+ }
1904
+ return null;
1905
+ }
1906
+ function optionBlock(help, flag) {
1907
+ const lines = help.split("\n");
1908
+ const re = new RegExp(`^\\s{0,4}(?:-\\w, )?${flag.replace(/-/g, "\\-")}(?![\\w-])`);
1909
+ const start = lines.findIndex((l) => re.test(l));
1910
+ if (start < 0) return null;
1911
+ const out = [lines[start]];
1912
+ for (let i = start + 1; i < lines.length && lines[i]?.trim() && !/^\s{0,4}-/.test(lines[i]); i++)
1913
+ out.push(lines[i]);
1914
+ return out.join(" ");
1915
+ }
1916
+ function parseCaps(help) {
1917
+ const pm = optionBlock(help, "--permission-mode");
1918
+ const pp = optionBlock(help, "--permission-prompts");
1919
+ return {
1920
+ hasMode: !!pm,
1921
+ modes: pm ? [...pm.matchAll(/"([A-Za-z]+)"/g)].map((m) => m[1]) : [],
1922
+ promptsNone: !!pp && /"none"/.test(pp),
1923
+ fork: !!optionBlock(help, "--fork-session"),
1924
+ stream: !!optionBlock(help, "--include-partial-messages")
1925
+ };
1926
+ }
1927
+ var LEVEL_MODE = {
1928
+ read: "plan",
1929
+ edits: "acceptEdits",
1930
+ auto: "auto"
1931
+ };
1932
+ function modeFlags(level, caps) {
1933
+ if (level === "inherit") return { args: [] };
1934
+ if (caps.unread)
1935
+ return { error: `this machine couldn't read its Claude Code's options (${caps.unread}); try again in a minute` };
1936
+ if (level === "auto" && !caps.modes.includes("auto"))
1937
+ return { error: "this machine is in auto mode, but its Claude Code has no auto permission mode" };
1938
+ if (level !== "safe") {
1939
+ const m = LEVEL_MODE[level];
1940
+ if (!caps.modes.includes(m)) return { error: `this machine's Claude Code has no ${m} permission mode` };
1941
+ return { args: ["--permission-mode", m] };
1942
+ }
1943
+ if (caps.modes.includes("dontAsk")) return { args: ["--permission-mode", "dontAsk"] };
1944
+ const args = [];
1945
+ if (caps.hasMode)
1946
+ args.push(
1947
+ "--permission-mode",
1948
+ caps.modes.includes("manual") && !caps.modes.includes("default") ? "manual" : "default"
1949
+ );
1950
+ if (caps.promptsNone) args.push("--permission-prompts", "none");
1951
+ return { args };
1952
+ }
1953
+ var ceilingLevel = (mode) => mode === "inherit" ? "safe" : mode;
1954
+ function offeredLevels(mode, caps) {
1955
+ if (caps.unread) return [];
1956
+ return START_PERMISSIONS.filter(
1957
+ (l) => levelRank(l) <= levelRank(ceilingLevel(mode)) && (l === "safe" || caps.modes.includes(LEVEL_MODE[l]))
1958
+ );
1959
+ }
1960
+ var defaultLevel = (mode) => mode;
1961
+ function outputArgs(caps) {
1962
+ return caps.stream ? ["--output-format", "stream-json", "--verbose", "--include-partial-messages"] : ["--output-format", "json"];
1963
+ }
1964
+ function claudeArgs(o) {
1965
+ const mf = modeFlags(o.level, o.caps);
1966
+ if ("error" in mf) return mf;
1967
+ const where = o.resume ? ["--resume", o.resume, ...o.fork ? ["--fork-session"] : []] : ["--session-id", o.session];
1968
+ return {
1969
+ args: [
1970
+ "-p",
1971
+ ...outputArgs(o.caps),
1972
+ ...mf.args,
1973
+ ...o.model ? ["--model", o.model] : [],
1974
+ ...o.allowedTools?.length ? [`--allowedTools=${o.allowedTools.join(",")}`] : [],
1975
+ ...where,
1976
+ o.prompt.startsWith("-") ? ` ${o.prompt}` : o.prompt
1977
+ ]
1978
+ };
1979
+ }
1980
+ function claudeEnv(configDir, env = process.env) {
1981
+ const e = withoutLineage(env);
1982
+ for (const k of [
1983
+ "CLAUDECODE",
1984
+ "CLAUDE_CODE_SESSION_ID",
1985
+ "CLAUDE_CODE_HOST_SESSION_ID",
1986
+ "CLAUDE_CODE_ENTRYPOINT",
1987
+ "CLAUDE_CODE_SESSION_ATTENDED",
1988
+ "CLAUDE_CODE_SSE_PORT"
1989
+ ])
1990
+ delete e[k];
1991
+ e.SESSIONPIPE_CONTROL_JOB = "1";
1992
+ if (configDir !== void 0) {
1993
+ if (path7.resolve(configDir) === path7.resolve(os2.homedir(), ".claude")) delete e.CLAUDE_CONFIG_DIR;
1994
+ else e.CLAUDE_CONFIG_DIR = configDir;
1995
+ }
1996
+ return e;
1997
+ }
1998
+ function parseAuthStatus(stdout) {
1999
+ const text = String(stdout || "").trim();
2000
+ const start = text.indexOf("{");
2001
+ if (start < 0) return { loggedIn: null };
2002
+ try {
2003
+ const j = JSON.parse(text.slice(start));
2004
+ if (typeof j.loggedIn !== "boolean") return { loggedIn: null };
2005
+ return {
2006
+ loggedIn: j.loggedIn,
2007
+ ...typeof j.authMethod === "string" ? { method: j.authMethod } : {},
2008
+ ...typeof j.subscriptionType === "string" ? { subscription: j.subscriptionType } : {}
2009
+ };
2010
+ } catch {
2011
+ return { loggedIn: null };
2012
+ }
2013
+ }
2014
+ function claudeLogin2(bin, configDir, env = process.env) {
2015
+ const c = probe(bin, ["auth", "status", "--json"]);
2016
+ return new Promise((resolve) => {
2017
+ execFile(
2018
+ c.file,
2019
+ c.args,
2020
+ { encoding: "utf8", timeout: 2e4, env: claudeEnv(configDir, env), maxBuffer: 256 * 1024, shell: c.shell },
2021
+ // `auth status` may exit non-zero when signed out; its JSON still says so.
2022
+ (_err, stdout) => resolve(parseAuthStatus(stdout))
2023
+ );
2024
+ });
2025
+ }
2026
+ function loginCommand(configDir) {
2027
+ const home = os2.homedir();
2028
+ const isDefault = !configDir || path7.resolve(configDir) === path7.resolve(home, ".claude");
2029
+ return isDefault ? "`claude auth login`" : `\`CLAUDE_CONFIG_DIR=${configDir.replace(home, "~")} claude auth login\``;
2030
+ }
2031
+ function loginHint(configDir, platform = process.platform) {
2032
+ const shown = configDir ? configDir.replace(os2.homedir(), "~") : "~/.claude";
2033
+ const login = loginCommand(configDir);
2034
+ if (platform === "darwin")
2035
+ return `Claude Code (${shown}) isn't signed in as this machine's background service sees it. On a Mac its login lives in the login keychain, which a background service can read only while someone is logged in at the screen: log in at the Mac, or run ${login} there.`;
2036
+ return `Claude Code (${shown}) isn't signed in on this machine: run ${login} there.`;
2037
+ }
2038
+ function detectCaps(bin, timeoutMs = 3e4) {
2039
+ const c = probe(bin, ["--help"]);
2040
+ try {
2041
+ return parseCaps(
2042
+ execFileSync3(c.file, c.args, {
2043
+ encoding: "utf8",
2044
+ timeout: timeoutMs,
2045
+ env: claudeEnv(),
2046
+ stdio: ["ignore", "pipe", "pipe"],
2047
+ maxBuffer: 4 * 1024 * 1024,
2048
+ shell: c.shell
2049
+ })
2050
+ );
2051
+ } catch (e) {
2052
+ const err = e;
2053
+ const caps = parseCaps(String(err?.stdout || ""));
2054
+ if (caps.hasMode) return caps;
2055
+ const why = err?.code === "ETIMEDOUT" ? `timed out after ${timeoutMs / 1e3} s` : "failed";
2056
+ return { ...caps, unread: `\`${path7.basename(bin)} --help\` ${why}` };
2057
+ }
2058
+ }
2059
+ function transcriptIn(session, dir) {
2060
+ let projects = [];
2061
+ try {
2062
+ projects = readdirSync4(path7.join(dir, "projects"), { withFileTypes: true });
2063
+ } catch {
2064
+ return null;
2065
+ }
2066
+ for (const p of projects) {
2067
+ if (!p.isDirectory()) continue;
2068
+ const file = path7.join(dir, "projects", p.name, `${session}.jsonl`);
2069
+ try {
2070
+ if (statSync5(file).isFile()) return { file, configDir: dir, project: p.name };
2071
+ } catch {
2072
+ }
2073
+ }
2074
+ return null;
2075
+ }
2076
+ function findTranscript(session, dirs) {
2077
+ if (!UUID_RE.test(session)) return null;
2078
+ for (const dir of dirs) {
2079
+ const t = transcriptIn(session, dir);
2080
+ if (t) return t;
2081
+ }
2082
+ return null;
2083
+ }
2084
+ function findTranscripts(session, dirs) {
2085
+ if (!UUID_RE.test(session)) return [];
2086
+ const out = [];
2087
+ for (const dir of dirs) {
2088
+ const t = transcriptIn(session, dir);
2089
+ if (t) out.push(t);
2090
+ }
2091
+ return out;
2092
+ }
2093
+ var homeName = (t) => path7.basename(t.configDir);
2094
+ function resolveHome(session, dirs) {
2095
+ const all = findTranscripts(session, dirs);
2096
+ if (!all.length) return null;
2097
+ if (all.length === 1) return { transcript: all[0] };
2098
+ const live = all.filter((t) => liveRecords(session, t.configDir).length > 0);
2099
+ if (live.length === 1) return { transcript: live[0] };
2100
+ return {
2101
+ ambiguous: [...new Set(all.map(homeName))].sort(),
2102
+ live: [...new Set(live.map(homeName))].sort()
2103
+ };
2104
+ }
2105
+ var projectName = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
2106
+ function sessionFolder(t, cap2 = 8 * 1024 * 1024) {
2107
+ let first = null;
2108
+ let count = 0;
2109
+ let fd;
2110
+ const look = (line) => {
2111
+ if (line.indexOf('"cwd"') < 0) return null;
2112
+ let j;
2113
+ try {
2114
+ j = JSON.parse(line.toString("utf8"));
2115
+ } catch {
2116
+ return null;
2117
+ }
2118
+ if (!j || typeof j.cwd !== "string" || !path7.isAbsolute(j.cwd)) return null;
2119
+ if (projectName(j.cwd) === t.project) return j.cwd;
2120
+ first ??= j.cwd;
2121
+ count++;
2122
+ return null;
2123
+ };
2124
+ try {
2125
+ fd = openSync2(t.file, "r");
2126
+ const buf = Buffer.alloc(256 * 1024);
2127
+ let pos = 0;
2128
+ let rest = Buffer.alloc(0);
2129
+ let skipping = false;
2130
+ while (pos < cap2 && count < 50) {
2131
+ const n = readSync2(fd, buf, 0, buf.length, pos);
2132
+ if (!n) {
2133
+ if (!skipping && rest.length) {
2134
+ const hit = look(rest);
2135
+ if (hit) return hit;
2136
+ }
2137
+ break;
2138
+ }
2139
+ pos += n;
2140
+ let chunk = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
2141
+ let nl = chunk.indexOf(10);
2142
+ while (nl >= 0) {
2143
+ const line = chunk.subarray(0, nl);
2144
+ chunk = chunk.subarray(nl + 1);
2145
+ if (skipping) skipping = false;
2146
+ else {
2147
+ const hit = look(line);
2148
+ if (hit) return hit;
2149
+ }
2150
+ nl = chunk.indexOf(10);
2151
+ }
2152
+ if (chunk.length > 2 * 1024 * 1024) {
2153
+ skipping = true;
2154
+ rest = Buffer.alloc(0);
2155
+ } else rest = skipping ? Buffer.alloc(0) : Buffer.from(chunk);
2156
+ }
2157
+ } catch {
2158
+ } finally {
2159
+ if (fd !== void 0)
2160
+ try {
2161
+ closeSync2(fd);
2162
+ } catch {
2163
+ }
2164
+ }
2165
+ return first;
2166
+ }
2167
+ function pidAlive(pid) {
2168
+ if (!Number.isSafeInteger(pid) || pid <= 1) return false;
2169
+ try {
2170
+ process.kill(pid, 0);
2171
+ return true;
2172
+ } catch (e) {
2173
+ return e.code === "EPERM";
2174
+ }
2175
+ }
2176
+ var registryRow = (file, configDir, j) => {
2177
+ const pid = Number(j.pid ?? path7.basename(file).slice(0, -5));
2178
+ const str = (v) => typeof v === "string" && v ? v : null;
2179
+ let keyFile = null;
2180
+ try {
2181
+ const want = new RegExp(`^${pid}\\.[0-9a-f]{64}\\.key$`);
2182
+ const hit = readdirSync4(path7.dirname(file)).find((n) => want.test(n));
2183
+ if (hit) keyFile = path7.join(path7.dirname(file), hit);
2184
+ } catch {
2185
+ }
2186
+ return {
2187
+ configDir,
2188
+ file,
2189
+ pid,
2190
+ socket: str(j.messagingSocketPath),
2191
+ peerProtocol: typeof j.peerProtocol === "number" ? j.peerProtocol : null,
2192
+ procStart: str(j.procStart),
2193
+ status: str(j.status),
2194
+ kind: str(j.kind),
2195
+ entrypoint: str(j.entrypoint),
2196
+ keyFile
2197
+ };
2198
+ };
2199
+ function liveRecords(session, dirs) {
2200
+ const out = [];
2201
+ for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
2202
+ let names = [];
2203
+ try {
2204
+ names = readdirSync4(path7.join(dir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
2205
+ } catch {
2206
+ continue;
2207
+ }
2208
+ for (const n of names) {
2209
+ const file = path7.join(dir, "sessions", n);
2210
+ let j;
2211
+ try {
2212
+ j = JSON.parse(readFileSync5(file, "utf8"));
2213
+ } catch {
2214
+ continue;
2215
+ }
2216
+ if (String(j.sessionId) !== session) continue;
2217
+ const row = registryRow(file, dir, j);
2218
+ if (pidAlive(row.pid)) out.push(row);
2219
+ }
2220
+ }
2221
+ return out;
2222
+ }
2223
+ function liveRecord(session, dirs) {
2224
+ const all = liveRecords(session, dirs);
2225
+ return all.length === 1 ? all[0] : null;
2226
+ }
2227
+ function liveProcess(session, configDir) {
2228
+ return liveRecords(session, configDir).length > 0;
2229
+ }
2230
+ var MODE_RANK = {
2231
+ plan: 0,
2232
+ default: 1,
2233
+ manual: 1,
2234
+ dontAsk: 1,
2235
+ acceptEdits: 2,
2236
+ auto: 3,
2237
+ bypassPermissions: 4
2238
+ };
2239
+ var CLAUDE_MODES = Object.keys(MODE_RANK);
2240
+ var modeRank = (m) => m && m in MODE_RANK ? MODE_RANK[m] : -1;
2241
+ function hookMode(payload) {
2242
+ if (!payload || typeof payload !== "object") return null;
2243
+ const p = payload;
2244
+ const v = typeof p.permission_mode === "string" ? p.permission_mode : p.permissionMode;
2245
+ return typeof v === "string" && CLAUDE_MODES.includes(v) ? v : null;
2246
+ }
2247
+ function peerClass(mode) {
2248
+ if (mode === "bypassPermissions") return "bypass";
2249
+ if (mode === "default" || mode === "manual" || mode === "dontAsk" || mode === "acceptEdits") return "prompting";
2250
+ return null;
2251
+ }
2252
+ var NO_HOME_SETTINGS = { defaultMode: null, disableAutoMode: false, disableBypass: false };
2253
+ function homeSettings(configDir, home = os2.homedir()) {
2254
+ const file = path7.join(configDir ?? path7.join(home, ".claude"), "settings.json");
2255
+ let j;
2256
+ try {
2257
+ j = JSON.parse(readFileSync5(file, "utf8"));
2258
+ } catch {
2259
+ return { ...NO_HOME_SETTINGS };
2260
+ }
2261
+ if (!j || typeof j !== "object" || Array.isArray(j)) return { ...NO_HOME_SETTINGS };
2262
+ const p = j.permissions && typeof j.permissions === "object" ? j.permissions : {};
2263
+ const off = (k) => j[k] === "disable" || p[k] === "disable";
2264
+ return {
2265
+ defaultMode: typeof p.defaultMode === "string" && CLAUDE_MODES.includes(p.defaultMode) ? p.defaultMode : null,
2266
+ disableAutoMode: off("disableAutoMode"),
2267
+ disableBypass: off("disableBypassPermissionsMode")
2268
+ };
2269
+ }
2270
+ function relaunchMode(recorded, home) {
2271
+ if (recorded === "plan") return recorded;
2272
+ if ((recorded === "dontAsk" || recorded === "acceptEdits") && modeRank(home.defaultMode) >= modeRank(recorded))
2273
+ return recorded;
2274
+ return null;
2275
+ }
2276
+ function recentlyWritten(file, ourLastEnd, now = Date.now()) {
2277
+ let m;
2278
+ try {
2279
+ m = statSync5(file).mtimeMs;
2280
+ } catch {
2281
+ return false;
2282
+ }
2283
+ if (now - m > LIVE_WRITE_MS) return false;
2284
+ return !(ourLastEnd && m <= ourLastEnd + 2e3);
2285
+ }
2286
+ function runClaude(bin, args, o) {
2287
+ return runHeadless(bin, args, { ...o, keep: o.keep ?? ((line) => line.includes('"type":"result"')) });
2288
+ }
2289
+ function parseResult(stdout) {
2290
+ const s = String(stdout || "").trim();
2291
+ if (!s) return null;
2292
+ const pick = (j) => {
2293
+ if (Array.isArray(j)) {
2294
+ for (let i = j.length - 1; i >= 0; i--) if (j[i]?.type === "result") return j[i];
2295
+ return null;
2296
+ }
2297
+ return j && typeof j === "object" ? j : null;
2298
+ };
2299
+ try {
2300
+ return pick(JSON.parse(s));
2301
+ } catch {
2302
+ }
2303
+ const lines = s.split("\n");
2304
+ for (let i = lines.length - 1; i >= 0; i--) {
2305
+ try {
2306
+ const j = pick(JSON.parse(lines[i]));
2307
+ if (j && (j.type === "result" || "result" in j)) return j;
2308
+ } catch {
2309
+ }
2310
+ }
2311
+ return null;
2312
+ }
2313
+ var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
2314
+
1618
2315
  // ../core/src/adapters/codex.ts
1619
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1620
- import path7 from "node:path";
2316
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
2317
+ import path8 from "node:path";
1621
2318
 
1622
2319
  // ../core/src/adapters/codex-config.ts
1623
2320
  var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
@@ -1763,35 +2460,36 @@ var CODEX_EVENTS = [
1763
2460
  ];
1764
2461
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1765
2462
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1766
- var codexHome = (env) => env.CODEX_HOME || path7.join(homeDir(), ".codex");
1767
- var hooksFile2 = (env) => path7.join(codexHome(env), "hooks.json");
1768
- var configFile2 = (env) => path7.join(codexHome(env), "config.toml");
1769
- var sessionsDir = (env) => path7.join(codexHome(env), "sessions");
2463
+ var codexHome = (env) => env.CODEX_HOME || path8.join(homeDir(), ".codex");
2464
+ var hooksFile2 = (env) => path8.join(codexHome(env), "hooks.json");
2465
+ var configFile2 = (env) => path8.join(codexHome(env), "config.toml");
2466
+ var sessionsDir = (env) => path8.join(codexHome(env), "sessions");
1770
2467
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1771
2468
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
2469
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
1772
2470
  function installNotify(cmd, env) {
1773
2471
  const file = configFile2(env);
1774
2472
  let text = "";
2473
+ let existed = true;
1775
2474
  try {
1776
- text = readFileSync5(file, "utf8");
1777
- } catch {
2475
+ text = readFileSync6(file, "utf8");
2476
+ } catch (e) {
2477
+ existed = e.code !== "ENOENT";
1778
2478
  }
1779
2479
  const argv = cmd([NAME3, "notify"]).argv;
1780
2480
  const line = `notify = ${JSON.stringify(argv)}`;
1781
2481
  const n = readNotify(text);
1782
- const write = (next) => {
1783
- mkdirSync3(path7.dirname(file), { recursive: true });
1784
- writeFileSync4(file, next);
2482
+ const write = (next, note) => {
2483
+ const bad = writeBack(file, next, existed);
2484
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
1785
2485
  };
1786
2486
  if (n && !n.other) {
1787
2487
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1788
2488
  return { file, changed: false, note: "notify current" };
1789
- write(withNotify(text, line));
1790
- return {
1791
- file,
1792
- changed: true,
1793
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1794
- };
2489
+ return write(
2490
+ withNotify(text, line),
2491
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
2492
+ );
1795
2493
  }
1796
2494
  if (n?.other) {
1797
2495
  const prev = previousNotify(n.argv);
@@ -1800,40 +2498,38 @@ function installNotify(cmd, env) {
1800
2498
  if (JSON.stringify(prev) === JSON.stringify(argv))
1801
2499
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1802
2500
  }
1803
- if (!prev || OURS2.test(JSON.stringify(prev))) {
1804
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1805
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1806
- }
2501
+ if (!prev || OURS2.test(JSON.stringify(prev)))
2502
+ return write(
2503
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
2504
+ "chained after Codex Computer Use (--previous-notify)"
2505
+ );
1807
2506
  }
1808
2507
  return {
1809
2508
  file,
1810
2509
  changed: false,
1811
2510
  skipped: true,
1812
- note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. Codex will report nothing until you approve the hooks: start \`codex\` and accept the hook review`
2511
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
1813
2512
  };
1814
2513
  }
1815
- write(withNotify(text, line));
1816
- return { file, changed: true, note: "notify set at the top level" };
2514
+ return write(withNotify(text, line), "notify set at the top level");
1817
2515
  }
1818
2516
  function uninstallNotify(env) {
1819
2517
  const file = configFile2(env);
1820
2518
  let text;
1821
2519
  try {
1822
- text = readFileSync5(file, "utf8");
2520
+ text = readFileSync6(file, "utf8");
1823
2521
  } catch {
1824
2522
  return { file, changed: false };
1825
2523
  }
1826
2524
  const n = readNotify(text);
1827
2525
  if (!n) return { file, changed: false };
1828
- if (!n.other) {
1829
- writeFileSync4(file, withoutNotify(text));
1830
- return { file, changed: true };
1831
- }
2526
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
1832
2527
  const prev = previousNotify(n.argv);
1833
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1834
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1835
- return { file, changed: true };
1836
- }
2528
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
2529
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
2530
+ file,
2531
+ changed: true
2532
+ };
1837
2533
  return { file, changed: false };
1838
2534
  }
1839
2535
  function findRollout(threadId, env = process.env) {
@@ -1842,12 +2538,12 @@ function findRollout(threadId, env = process.env) {
1842
2538
  const walk = (dir, depth) => {
1843
2539
  let names = [];
1844
2540
  try {
1845
- names = readdirSync4(dir);
2541
+ names = readdirSync5(dir);
1846
2542
  } catch {
1847
2543
  return;
1848
2544
  }
1849
2545
  for (const n of names.sort().reverse()) {
1850
- const p = path7.join(dir, n);
2546
+ const p = path8.join(dir, n);
1851
2547
  if (n.endsWith(`-${threadId}.jsonl`)) {
1852
2548
  best = p;
1853
2549
  return;
@@ -1865,8 +2561,12 @@ function rolloutMeta(file) {
1865
2561
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
1866
2562
  }
1867
2563
  function ofAnotherThread(file, id) {
2564
+ const other = threadOf(file);
2565
+ return !!other && other !== id.toLowerCase();
2566
+ }
2567
+ function threadOf(file) {
1868
2568
  const m = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i.exec(file);
1869
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
2569
+ return m?.[1] ? m[1].toLowerCase() : null;
1870
2570
  }
1871
2571
  var codex = {
1872
2572
  name: NAME3,
@@ -1891,7 +2591,7 @@ var codex = {
1891
2591
  ];
1892
2592
  let text = "";
1893
2593
  try {
1894
- text = readFileSync5(configFile2(env), "utf8");
2594
+ text = readFileSync6(configFile2(env), "utf8");
1895
2595
  } catch {
1896
2596
  }
1897
2597
  const n = readNotify(text);
@@ -1945,6 +2645,8 @@ var codex = {
1945
2645
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1946
2646
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1947
2647
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2648
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
2649
+ const agent_id = typeof s.agent_id === "string" && s.agent_id ? s.agent_id : typeof s.subagent_id === "string" && s.subagent_id ? s.subagent_id : thread && thread !== String(id).toLowerCase() ? thread : void 0;
1948
2650
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1949
2651
  const events = [];
1950
2652
  switch (event) {
@@ -1964,17 +2666,30 @@ var codex = {
1964
2666
  );
1965
2667
  break;
1966
2668
  case "PreToolUse":
1967
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2669
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
1968
2670
  break;
1969
2671
  case "PostToolUse":
1970
2672
  if (tool)
1971
2673
  events.push(
1972
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
2674
+ ev("tool.ended", {
2675
+ tool,
2676
+ call_id,
2677
+ turn_id,
2678
+ agent_id,
2679
+ ok: true,
2680
+ input: s.tool_input,
2681
+ output: s.tool_response
2682
+ })
1973
2683
  );
1974
2684
  break;
1975
2685
  case "PermissionRequest":
1976
2686
  events.push(
1977
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
2687
+ ev("attention.needed", {
2688
+ attention_id: call_id ?? `perm-${Date.now()}`,
2689
+ kind: "permission",
2690
+ tool,
2691
+ agent_id
2692
+ })
1978
2693
  );
1979
2694
  break;
1980
2695
  case "Stop":
@@ -2018,7 +2733,7 @@ var codex = {
2018
2733
  readTranscript(file, fromLine) {
2019
2734
  let text = "";
2020
2735
  try {
2021
- text = readFileSync5(file, "utf8");
2736
+ text = readFileSync6(file, "utf8");
2022
2737
  } catch {
2023
2738
  return { turns: [], line: fromLine };
2024
2739
  }
@@ -2032,7 +2747,7 @@ var codex = {
2032
2747
  backfill(sinceMs, env = process.env) {
2033
2748
  const rows2 = [];
2034
2749
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
2035
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path7.basename(f));
2750
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path8.basename(f));
2036
2751
  if (!m?.[1]) continue;
2037
2752
  if (codexSubagent(rolloutMeta(f))) continue;
2038
2753
  const facts = codexFacts(f);
@@ -2119,10 +2834,10 @@ function codexFacts(file) {
2119
2834
  // ../core/src/adapters/devin.ts
2120
2835
  import { createHash as createHash3 } from "node:crypto";
2121
2836
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
2122
- import path8 from "node:path";
2837
+ import path9 from "node:path";
2123
2838
 
2124
2839
  // ../core/src/readers/sqlite.ts
2125
- import { execFileSync as execFileSync3 } from "node:child_process";
2840
+ import { execFileSync as execFileSync4 } from "node:child_process";
2126
2841
  import { existsSync as existsSync5 } from "node:fs";
2127
2842
  import { createRequire } from "node:module";
2128
2843
  function quiet(fn) {
@@ -2202,7 +2917,7 @@ db.close();
2202
2917
  process.stdout.write(JSON.stringify(out));`;
2203
2918
  function viaChild(file, sql, params, o) {
2204
2919
  try {
2205
- const out = execFileSync3(
2920
+ const out = execFileSync4(
2206
2921
  process.execPath,
2207
2922
  ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
2208
2923
  {
@@ -2246,7 +2961,7 @@ function viaCli(file, sql, params, o) {
2246
2961
  const text = inlineParams(sql, params);
2247
2962
  if (!text) return null;
2248
2963
  try {
2249
- const out = execFileSync3("sqlite3", ["-readonly", "-json", file, text], {
2964
+ const out = execFileSync4("sqlite3", ["-readonly", "-json", file, text], {
2250
2965
  encoding: "utf8",
2251
2966
  timeout: o.timeoutMs + 3e3,
2252
2967
  stdio: ["ignore", "pipe", "ignore"],
@@ -2303,11 +3018,11 @@ function foreignEvent(file) {
2303
3018
  return Object.keys(hooks).find((k) => !ok.includes(k));
2304
3019
  }
2305
3020
  var homeOf = (env) => env.HOME || homeDir();
2306
- var appData = (env) => env.APPDATA || path8.join(homeOf(env), "AppData", "Roaming");
2307
- var devinConfigDir = (env = process.env) => process.platform === "win32" ? path8.join(appData(env), "devin") : path8.join(env.XDG_CONFIG_HOME || path8.join(homeOf(env), ".config"), "devin");
2308
- var configFile3 = (env) => path8.join(devinConfigDir(env), "config.json");
2309
- var devinDataDir = (env = process.env) => process.platform === "win32" ? path8.join(appData(env), "devin", "cli") : path8.join(env.XDG_DATA_HOME || path8.join(homeOf(env), ".local", "share"), "devin", "cli");
2310
- var sessionsDb = (env) => path8.join(devinDataDir(env), "sessions.db");
3021
+ var appData = (env) => env.APPDATA || path9.join(homeOf(env), "AppData", "Roaming");
3022
+ var devinConfigDir = (env = process.env) => process.platform === "win32" ? path9.join(appData(env), "devin") : path9.join(env.XDG_CONFIG_HOME || path9.join(homeOf(env), ".config"), "devin");
3023
+ var configFile3 = (env) => path9.join(devinConfigDir(env), "config.json");
3024
+ var devinDataDir = (env = process.env) => process.platform === "win32" ? path9.join(appData(env), "devin", "cli") : path9.join(env.XDG_DATA_HOME || path9.join(homeOf(env), ".local", "share"), "devin", "cli");
3025
+ var sessionsDb = (env) => path9.join(devinDataDir(env), "sessions.db");
2311
3026
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
2312
3027
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2313
3028
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2464,7 +3179,7 @@ var CHAIN_LIMIT = 4e3;
2464
3179
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2465
3180
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2466
3181
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2467
- UNION ALL
3182
+ UNION
2468
3183
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2469
3184
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2470
3185
  )
@@ -2479,7 +3194,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2479
3194
  function sessionFacts(row) {
2480
3195
  const out = {};
2481
3196
  if (typeof row.model === "string" && row.model) out.model = row.model;
2482
- if (typeof row.working_directory === "string" && path8.isAbsolute(row.working_directory))
3197
+ if (typeof row.working_directory === "string" && path9.isAbsolute(row.working_directory))
2483
3198
  out.cwd = row.working_directory;
2484
3199
  const started = Number(row.created_at);
2485
3200
  const last = Number(row.last_activity_at);
@@ -2524,8 +3239,8 @@ function readDevinChain(db, session, fromLine) {
2524
3239
  }
2525
3240
  function devinVersion(env = process.env) {
2526
3241
  try {
2527
- const target = readlinkSync(path8.join(devinDataDir(env), "_versions", "current"));
2528
- const name = path8.basename(target);
3242
+ const target = readlinkSync(path9.join(devinDataDir(env), "_versions", "current"));
3243
+ const name = path9.basename(target);
2529
3244
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2530
3245
  } catch {
2531
3246
  return void 0;
@@ -2538,9 +3253,9 @@ function devinAccount(env = process.env) {
2538
3253
  }
2539
3254
 
2540
3255
  // ../core/src/adapters/gemini-cli.ts
2541
- import { execFileSync as execFileSync4 } from "node:child_process";
2542
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2543
- import path9 from "node:path";
3256
+ import { execFileSync as execFileSync5 } from "node:child_process";
3257
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
3258
+ import path10 from "node:path";
2544
3259
  var NAME5 = "gemini-cli";
2545
3260
  var GEMINI_EVENTS = [
2546
3261
  "SessionStart",
@@ -2553,16 +3268,16 @@ var GEMINI_EVENTS = [
2553
3268
  "SessionEnd"
2554
3269
  ];
2555
3270
  var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2556
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path9.join(homeDir(), ".gemini");
2557
- var settings = (env) => path9.join(geminiDir(env), "settings.json");
3271
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path10.join(homeDir(), ".gemini");
3272
+ var settings = (env) => path10.join(geminiDir(env), "settings.json");
2558
3273
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2559
3274
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2560
3275
  function geminiVersion(platform = process.platform, env = process.env) {
2561
3276
  const win = platform === "win32";
2562
- const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path9.dirname(process.execPath)], env }) : "gemini";
3277
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path10.dirname(process.execPath)], env }) : "gemini";
2563
3278
  if (!bin) throw new Error("no gemini on PATH");
2564
3279
  const c = probe(bin, ["--version"]);
2565
- return execFileSync4(c.file, c.args, {
3280
+ return execFileSync5(c.file, c.args, {
2566
3281
  encoding: "utf8",
2567
3282
  timeout: 4e3,
2568
3283
  stdio: ["ignore", "pipe", "ignore"],
@@ -2595,7 +3310,7 @@ var geminiCli = {
2595
3310
  name: NAME5,
2596
3311
  events: GEMINI_EVENTS,
2597
3312
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2598
- detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path9.join(geminiDir(env), "tmp")),
3313
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path10.join(geminiDir(env), "tmp")),
2599
3314
  configFiles: (env = process.env) => [settings(env)],
2600
3315
  install: (cmd, env = process.env, opts = {}) => {
2601
3316
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2693,7 +3408,7 @@ var geminiCli = {
2693
3408
  readTranscript(file, fromLine) {
2694
3409
  let text = "";
2695
3410
  try {
2696
- text = readFileSync6(file, "utf8");
3411
+ text = readFileSync7(file, "utf8");
2697
3412
  } catch {
2698
3413
  return { turns: [], line: fromLine };
2699
3414
  }
@@ -2733,12 +3448,12 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2733
3448
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2734
3449
 
2735
3450
  // ../core/src/config.ts
2736
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
2737
- import path10 from "node:path";
3451
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
3452
+ import path11 from "node:path";
2738
3453
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2739
3454
  function readConfig(file = configFile()) {
2740
3455
  try {
2741
- const j = JSON.parse(readFileSync7(file, "utf8"));
3456
+ const j = JSON.parse(readFileSync8(file, "utf8"));
2742
3457
  return {
2743
3458
  ...DEFAULT_CONFIG,
2744
3459
  ...j,
@@ -2749,9 +3464,36 @@ function readConfig(file = configFile()) {
2749
3464
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
2750
3465
  }
2751
3466
  }
3467
+ function configProblem(file) {
3468
+ let text;
3469
+ try {
3470
+ text = readFileSync8(file, "utf8");
3471
+ } catch (e) {
3472
+ const code = e.code;
3473
+ return code === "ENOENT" ? void 0 : `could not be read (${code})`;
3474
+ }
3475
+ if (!text.trim()) return void 0;
3476
+ if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
3477
+ try {
3478
+ const j = JSON.parse(text);
3479
+ return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
3480
+ } catch (e) {
3481
+ const m = e.message;
3482
+ const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
3483
+ return `is not valid JSON${at ? ` at ${at}` : ""}`;
3484
+ }
3485
+ }
3486
+ function assertConfigWritable(file = configFile()) {
3487
+ const problem = configProblem(file);
3488
+ if (problem)
3489
+ throw new Error(
3490
+ `${tilde(file)} ${problem}, so it was not written over: that would lose its machine name and sinks. Fix it, or move it aside to start again, then run this again.`
3491
+ );
3492
+ }
2752
3493
  function writeConfig(c, file = configFile()) {
2753
- mkdirSync4(path10.dirname(file), { recursive: true, mode: 448 });
2754
- writeFileSync5(file, `${JSON.stringify(c, null, 2)}
3494
+ assertConfigWritable(file);
3495
+ mkdirSync2(path11.dirname(file), { recursive: true, mode: 448 });
3496
+ writeFileSync2(file, `${JSON.stringify(c, null, 2)}
2755
3497
  `, { mode: 384 });
2756
3498
  if (process.platform !== "win32" && existsSync8(file)) {
2757
3499
  try {
@@ -2814,38 +3556,23 @@ function makeEvent(a, ctx) {
2814
3556
  };
2815
3557
  }
2816
3558
 
2817
- // ../core/src/launched-by.ts
2818
- var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2819
- var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2820
- function launchedBy(env) {
2821
- const v = env[LAUNCHED_BY_ENV]?.trim();
2822
- return v && LAUNCHED_BY.test(v) ? v : void 0;
2823
- }
2824
- function readHook(adapter, input) {
2825
- const r = adapter.fromHook(input);
2826
- if (r && !r.session.parent_id) {
2827
- const launcher = launchedBy(input.env);
2828
- if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2829
- }
2830
- return r;
2831
- }
2832
-
2833
3559
  // ../core/src/outbox.ts
3560
+ import { randomBytes as randomBytes2 } from "node:crypto";
2834
3561
  import {
2835
3562
  appendFileSync,
2836
- closeSync as closeSync2,
3563
+ closeSync as closeSync3,
2837
3564
  existsSync as existsSync9,
2838
- mkdirSync as mkdirSync5,
2839
- openSync as openSync2,
2840
- readdirSync as readdirSync5,
2841
- readFileSync as readFileSync8,
2842
- readSync as readSync2,
3565
+ mkdirSync as mkdirSync3,
3566
+ openSync as openSync3,
3567
+ readdirSync as readdirSync6,
3568
+ readFileSync as readFileSync9,
3569
+ readSync as readSync3,
2843
3570
  renameSync,
2844
- statSync as statSync5,
2845
- unlinkSync as unlinkSync3,
2846
- writeFileSync as writeFileSync6
3571
+ statSync as statSync6,
3572
+ unlinkSync as unlinkSync2,
3573
+ writeFileSync as writeFileSync3
2847
3574
  } from "node:fs";
2848
- import path11 from "node:path";
3575
+ import path12 from "node:path";
2849
3576
  var READ_CHUNK = 1024 * 1024;
2850
3577
  var READ_MAX = 8 * 1024 * 1024;
2851
3578
  var Outbox = class {
@@ -2854,13 +3581,13 @@ var Outbox = class {
2854
3581
  }
2855
3582
  root;
2856
3583
  dir(harness) {
2857
- return path11.join(this.root, "outbox", safeId(harness));
3584
+ return path12.join(this.root, "outbox", safeId(harness));
2858
3585
  }
2859
3586
  file(ref) {
2860
- return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
3587
+ return path12.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2861
3588
  }
2862
3589
  seqFile(ref) {
2863
- return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
3590
+ return path12.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2864
3591
  }
2865
3592
  /** The next seq for a session, persisted (survives a hibernating machine and a
2866
3593
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2868,19 +3595,19 @@ var Outbox = class {
2868
3595
  const f = this.seqFile(ref);
2869
3596
  let n = -1;
2870
3597
  try {
2871
- const t = readFileSync8(f, "utf8").trim();
3598
+ const t = readFileSync9(f, "utf8").trim();
2872
3599
  if (/^\d+$/.test(t)) n = Number(t);
2873
3600
  } catch {
2874
3601
  }
2875
3602
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
2876
- mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2877
- writeFileSync6(f, String(n + 1), { mode: 384 });
3603
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3604
+ writeFileSync3(f, String(n + 1), { mode: 384 });
2878
3605
  return n;
2879
3606
  }
2880
3607
  lastSeqInFile(ref) {
2881
3608
  const f = this.file(ref);
2882
3609
  if (!existsSync9(f)) return -1;
2883
- const lines = readFileSync8(f, "utf8").trim().split("\n");
3610
+ const lines = readFileSync9(f, "utf8").trim().split("\n");
2884
3611
  for (let i = lines.length - 1; i >= 0; i--) {
2885
3612
  try {
2886
3613
  const j = JSON.parse(lines[i]);
@@ -2892,26 +3619,26 @@ var Outbox = class {
2892
3619
  }
2893
3620
  append(ref, events) {
2894
3621
  if (!events.length) return;
2895
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
3622
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
2896
3623
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
2897
3624
  `, { mode: 384 });
2898
3625
  }
2899
3626
  /** Every session file, newest first. */
2900
3627
  sessions() {
2901
3628
  const out = [];
2902
- const base = path11.join(this.root, "outbox");
3629
+ const base = path12.join(this.root, "outbox");
2903
3630
  if (!existsSync9(base)) return [];
2904
- for (const h of readdirSync5(base)) {
2905
- const d = path11.join(base, h);
3631
+ for (const h of readdirSync6(base)) {
3632
+ const d = path12.join(base, h);
2906
3633
  let names = [];
2907
3634
  try {
2908
- names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
3635
+ names = readdirSync6(d).filter((n) => n.endsWith(".jsonl"));
2909
3636
  } catch {
2910
3637
  continue;
2911
3638
  }
2912
3639
  for (const n of names) {
2913
3640
  try {
2914
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path11.join(d, n)).mtimeMs });
3641
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(path12.join(d, n)).mtimeMs });
2915
3642
  } catch {
2916
3643
  }
2917
3644
  }
@@ -2927,16 +3654,16 @@ var Outbox = class {
2927
3654
  read(ref, from, maxEvents = 500) {
2928
3655
  const f = this.file(ref);
2929
3656
  if (!existsSync9(f)) return { events: [], offset: from };
2930
- const size = statSync5(f).size;
3657
+ const size = statSync6(f).size;
2931
3658
  if (from >= size) return { events: [], offset: from };
2932
- const fd = openSync2(f, "r");
3659
+ const fd = openSync3(f, "r");
2933
3660
  try {
2934
3661
  const events = [];
2935
3662
  let offset = from;
2936
3663
  let chunk = READ_CHUNK;
2937
3664
  while (events.length < maxEvents && offset < size) {
2938
3665
  const buf = Buffer.alloc(Math.min(size - offset, chunk));
2939
- const n = readSync2(fd, buf, 0, buf.length, offset);
3666
+ const n = readSync3(fd, buf, 0, buf.length, offset);
2940
3667
  const text = buf.subarray(0, n).toString("utf8");
2941
3668
  let pos = 0;
2942
3669
  let used = 0;
@@ -2959,37 +3686,109 @@ var Outbox = class {
2959
3686
  }
2960
3687
  return { events, offset };
2961
3688
  } finally {
2962
- closeSync2(fd);
3689
+ closeSync3(fd);
2963
3690
  }
2964
3691
  }
2965
- /** Files older than `days` are pruned; the cursor files with them. */
3692
+ /** Files older than `days` are pruned, and each sink's cursor into them. The seq
3693
+ * counter stays: a session resumed after the window counts on from where it was,
3694
+ * so a receiver's (harness, session, seq) dedup doesn't take its new events for
3695
+ * ones it already has, and the sinks read the new file from its start.
3696
+ * It all happens under the drain's `flush` lock, since a drain holds the whole
3697
+ * cursor map and writes it back: a file and its cursor keys go together, so a
3698
+ * session resumed in between can't keep a key past its new file's end. While a
3699
+ * drain runs, nothing is pruned (0); the next prune does it. It never throws: it
3700
+ * is a cleanup, and a hook's drain comes after it. */
2966
3701
  prune(days, now = Date.now()) {
3702
+ let release = null;
3703
+ try {
3704
+ release = takeNamedLock(this.root, "flush");
3705
+ } catch {
3706
+ }
3707
+ if (!release) return 0;
2967
3708
  let n = 0;
2968
- const cutoff = now - days * 864e5;
2969
- for (const ref of this.sessions()) {
2970
- const f = this.file(ref);
2971
- try {
2972
- if (statSync5(f).mtimeMs < cutoff) {
2973
- unlinkSync3(f);
2974
- try {
2975
- unlinkSync3(this.seqFile(ref));
2976
- } catch {
3709
+ try {
3710
+ const cutoff = now - days * 864e5;
3711
+ const aged = this.sessions().filter((ref) => {
3712
+ try {
3713
+ return statSync6(this.file(ref)).mtimeMs < cutoff;
3714
+ } catch {
3715
+ return false;
3716
+ }
3717
+ });
3718
+ this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
3719
+ for (const ref of aged) {
3720
+ const f = this.file(ref);
3721
+ try {
3722
+ if (statSync6(f).mtimeMs < cutoff) {
3723
+ unlinkSync2(f);
3724
+ n++;
2977
3725
  }
2978
- n++;
3726
+ } catch {
2979
3727
  }
2980
- } catch {
2981
3728
  }
3729
+ } catch {
3730
+ } finally {
3731
+ release();
2982
3732
  }
2983
3733
  return n;
2984
3734
  }
3735
+ /** A sink's cursor into a session file about to be pruned (`gone`), or one already
3736
+ * missing (an earlier prune, an older release, by hand) is removed. The caller
3737
+ * holds the `flush` lock. */
3738
+ dropStaleCursors(gone) {
3739
+ let names = [];
3740
+ try {
3741
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3742
+ } catch {
3743
+ }
3744
+ const stale = (c) => Object.keys(c).filter((k) => {
3745
+ if (gone.has(k)) return true;
3746
+ const i = k.indexOf("/");
3747
+ return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
3748
+ });
3749
+ for (const n of names) {
3750
+ const s = new Cursors(this.root, n.slice(0, -5));
3751
+ const c = s.read();
3752
+ const keys = stale(c);
3753
+ if (!keys.length) continue;
3754
+ for (const k of keys) delete c[k];
3755
+ s.write(c);
3756
+ }
3757
+ }
2985
3758
  delete(ref) {
3759
+ this.dropCursors(ref);
2986
3760
  for (const f of [this.file(ref), this.seqFile(ref)]) {
2987
3761
  try {
2988
- unlinkSync3(f);
3762
+ unlinkSync2(f);
2989
3763
  } catch {
2990
3764
  }
2991
3765
  }
2992
3766
  }
3767
+ /** The key every sink's cursor holds for a session is removed with the session's
3768
+ * file: the key alone pointed into the old file, and a session that went on (still
3769
+ * running, or resumed later) had its new file's first events unsent until it grew
3770
+ * past that offset. Every file under <state>/cursors is taken (a sink since taken
3771
+ * out of the config still has one; one that doesn't parse reads as empty and is
3772
+ * left), by the key a drain writes (the file's own name, `safeId`) and by the ref
3773
+ * as given. The caller holds the drain's `flush` lock: a drain holds the whole
3774
+ * cursor map and writes it back, so a key dropped under it would come back. */
3775
+ dropCursors(ref) {
3776
+ const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
3777
+ let names = [];
3778
+ try {
3779
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3780
+ } catch {
3781
+ return;
3782
+ }
3783
+ for (const n of names) {
3784
+ const s = new Cursors(this.root, n.slice(0, -5));
3785
+ const c = s.read();
3786
+ const had = Object.keys(c).filter((k) => keys.has(k));
3787
+ if (!had.length) continue;
3788
+ for (const k of had) delete c[k];
3789
+ s.write(c);
3790
+ }
3791
+ }
2993
3792
  };
2994
3793
  var Cursors = class {
2995
3794
  constructor(root, sink) {
@@ -2999,20 +3798,23 @@ var Cursors = class {
2999
3798
  root;
3000
3799
  sink;
3001
3800
  file() {
3002
- return path11.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3801
+ return path12.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3003
3802
  }
3803
+ /** The map; empty when the file is missing, cut off mid-write, or holds anything
3804
+ * but an object (`null`, a list), so a drain never indexes into those. */
3004
3805
  read() {
3005
3806
  try {
3006
- return JSON.parse(readFileSync8(this.file(), "utf8"));
3807
+ const c = JSON.parse(readFileSync9(this.file(), "utf8"));
3808
+ return c && typeof c === "object" && !Array.isArray(c) ? c : {};
3007
3809
  } catch {
3008
3810
  return {};
3009
3811
  }
3010
3812
  }
3011
3813
  write(c) {
3012
3814
  const f = this.file();
3013
- mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
3815
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3014
3816
  const tmp = `${f}.${process.pid}.tmp`;
3015
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
3817
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
3016
3818
  renameSync(tmp, f);
3017
3819
  }
3018
3820
  key(ref) {
@@ -3023,22 +3825,44 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
3023
3825
  return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
3024
3826
  }
3025
3827
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
3026
- const dir = path11.join(root, "locks");
3027
- mkdirSync5(dir, { recursive: true, mode: 448 });
3028
- const f = path11.join(dir, `${name}.lock`);
3828
+ const dir = path12.join(root, "locks");
3829
+ mkdirSync3(dir, { recursive: true, mode: 448 });
3830
+ const f = path12.join(dir, `${name}.lock`);
3831
+ const nonce = randomBytes2(8).toString("hex");
3832
+ const mine = `${process.pid} ${nonce}`;
3833
+ let body = null;
3029
3834
  try {
3030
- if (Date.now() - statSync5(f).mtimeMs > staleMs || !processAlive(Number(readFileSync8(f, "utf8")))) unlinkSync3(f);
3031
- else return null;
3835
+ body = readFileSync9(f, "utf8");
3836
+ const stale = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
3837
+ if (!stale) return null;
3032
3838
  } catch {
3839
+ body = null;
3840
+ }
3841
+ if (body !== null) {
3842
+ const aside = `${f}.${process.pid}.${nonce}.stale`;
3843
+ try {
3844
+ renameSync(f, aside);
3845
+ } catch {
3846
+ return null;
3847
+ }
3848
+ const moved = readFileSync9(aside, "utf8");
3849
+ unlinkSync2(aside);
3850
+ if (moved !== body) {
3851
+ try {
3852
+ writeFileSync3(f, moved, { flag: "wx" });
3853
+ } catch {
3854
+ }
3855
+ return null;
3856
+ }
3033
3857
  }
3034
3858
  try {
3035
- writeFileSync6(f, String(process.pid), { flag: "wx" });
3859
+ writeFileSync3(f, mine, { flag: "wx" });
3036
3860
  } catch {
3037
3861
  return null;
3038
3862
  }
3039
3863
  return () => {
3040
3864
  try {
3041
- unlinkSync3(f);
3865
+ if (readFileSync9(f, "utf8") === mine) unlinkSync2(f);
3042
3866
  } catch {
3043
3867
  }
3044
3868
  };
@@ -3057,7 +3881,7 @@ function processAlive(pid) {
3057
3881
  import { createHash as createHash4 } from "node:crypto";
3058
3882
  var RULESET_PII = "pii@1";
3059
3883
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
3060
- var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
3884
+ var PHONE = /(?<![\w.])(?:(?<!\)[ .-]?|\+\d{1,3}[ .-]?)(\+?(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))(?:(?= (?:[01]?\d|2[0-3]):)|(?!\w|[ .-]\d\d))|(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w))/g;
3061
3885
  function looksPhone(s) {
3062
3886
  const digits = s.replace(/\D/g, "");
3063
3887
  if (digits.length < 7 || digits.length > 15) return false;
@@ -3068,7 +3892,7 @@ function looksPhone(s) {
3068
3892
  function reducePii(text, home) {
3069
3893
  if (!text) return text;
3070
3894
  let out = text.replace(EMAIL, "[email]");
3071
- out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
3895
+ out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
3072
3896
  if (home) {
3073
3897
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3074
3898
  out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -3265,6 +4089,8 @@ var TYPE_TIER = {
3265
4089
  "subagent.ended": 1,
3266
4090
  "context.compacted": 1,
3267
4091
  "limits.observed": 1,
4092
+ "delegation.started": 0,
4093
+ "delegation.ended": 0,
3268
4094
  native: 3
3269
4095
  };
3270
4096
  var DATA_ALLOW = {
@@ -3275,17 +4101,19 @@ var DATA_ALLOW = {
3275
4101
  "session.forgotten": { 0: [] },
3276
4102
  "turn.started": { 0: ["turn_id", "submitted_by", "injected"], 1: ["prompt_chars"] },
3277
4103
  "turn.ended": { 0: ["turn_id", "reason", "ms", "background"], 1: ["error"] },
3278
- "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
3279
- "turn.progress": { 2: ["turn", "user", "assistant", "at"] },
4104
+ "turn.transcript": { 2: ["turn", "user", "assistant", "at", "injected"] },
4105
+ "turn.progress": { 2: ["turn", "user", "assistant", "at", "injected"] },
3280
4106
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
3281
4107
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
3282
4108
  "files.changed": { 1: ["paths"] },
3283
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
4109
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
3284
4110
  "attention.cleared": { 0: ["attention_id", "how"] },
3285
4111
  "subagent.started": { 1: ["agent_id", "agent_type"] },
3286
4112
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
3287
4113
  "context.compacted": { 1: ["trigger"] },
3288
- "limits.observed": { 1: ["windows", "plan", "account_id"] }
4114
+ "limits.observed": { 1: ["windows", "plan", "account_id"] },
4115
+ "delegation.started": { 0: ["ask_id", "via", "harness", "to"], 1: ["text"] },
4116
+ "delegation.ended": { 0: ["ask_id", "to", "ok", "ms", "error", "relayed"], 2: ["reply"] }
3289
4117
  };
3290
4118
  var SESSION_ALLOW = [
3291
4119
  "id",
@@ -3301,7 +4129,8 @@ var SESSION_ALLOW = [
3301
4129
  "account_id",
3302
4130
  "attended",
3303
4131
  "parent_id",
3304
- "launched_by"
4132
+ "launched_by",
4133
+ "delegated_by"
3305
4134
  ];
3306
4135
  var MAX_SIDE = 2e4;
3307
4136
  var MAX_IO = 64 * 1024;
@@ -3340,6 +4169,8 @@ function filterEvent(event, tier, pii, opts = {}) {
3340
4169
  if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
3341
4170
  if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
3342
4171
  if (typeof data.message === "string") data.message = cap(data.message, 200);
4172
+ if (event.type === "delegation.started" && typeof data.text === "string") data.text = cap(data.text, 200);
4173
+ if (event.type === "delegation.ended" && typeof data.reply === "string") data.reply = cap(data.reply, MAX_SIDE);
3343
4174
  const rulesets = [];
3344
4175
  let out = { ...event, tier, session, data, privacy: { rulesets, pii } };
3345
4176
  if (tier > 0) {
@@ -3366,8 +4197,8 @@ function shortId(harness, id) {
3366
4197
  }
3367
4198
 
3368
4199
  // ../core/src/sinks/file.ts
3369
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3370
- import path12 from "node:path";
4200
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
4201
+ import path13 from "node:path";
3371
4202
  var FileSink = class {
3372
4203
  constructor(name, file, tier, pii) {
3373
4204
  this.name = name;
@@ -3381,7 +4212,7 @@ var FileSink = class {
3381
4212
  pii;
3382
4213
  async send(events) {
3383
4214
  try {
3384
- mkdirSync6(path12.dirname(this.file), { recursive: true });
4215
+ mkdirSync4(path13.dirname(this.file), { recursive: true });
3385
4216
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3386
4217
  `);
3387
4218
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3415,12 +4246,12 @@ var StdoutSink = class {
3415
4246
  };
3416
4247
 
3417
4248
  // src/control/aliases.ts
3418
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
3419
- import path13 from "node:path";
4249
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
4250
+ import path14 from "node:path";
3420
4251
 
3421
4252
  // src/secrets.ts
3422
4253
  import { spawnSync } from "node:child_process";
3423
- import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
4254
+ import { createHash as createHash6, randomBytes as randomBytes3 } from "node:crypto";
3424
4255
  var SERVICE = "sessionpipe";
3425
4256
  var defaultRunner = (cmd, args, input) => {
3426
4257
  const r = spawnSync(cmd, args, {
@@ -3444,9 +4275,9 @@ function secretGet(store, account, run = defaultRunner) {
3444
4275
  function sinkWithSecrets(s, run = defaultRunner) {
3445
4276
  if (!s.token_in) return s;
3446
4277
  const token = secretGet(s.token_in, sinkAccount(s.name), run);
3447
- if (!token) return null;
3448
4278
  const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
3449
- return { ...s, token, ...secret ? { secret } : {} };
4279
+ if (!token && !secret) return null;
4280
+ return { ...s, ...token ? { token } : {}, ...secret ? { secret } : {} };
3450
4281
  }
3451
4282
 
3452
4283
  // src/control/store.ts
@@ -3460,10 +4291,10 @@ function startOptionsOf(v) {
3460
4291
  }
3461
4292
 
3462
4293
  // src/control/aliases.ts
3463
- var aliasFile = (controlDir) => path13.join(controlDir, "aliases.json");
4294
+ var aliasFile = (controlDir) => path14.join(controlDir, "aliases.json");
3464
4295
  function readAliases(file) {
3465
4296
  try {
3466
- const j = JSON.parse(readFileSync9(file, "utf8"));
4297
+ const j = JSON.parse(readFileSync10(file, "utf8"));
3467
4298
  if (!j || typeof j !== "object" || Array.isArray(j)) return {};
3468
4299
  const out = {};
3469
4300
  for (const [k, v] of Object.entries(j)) {
@@ -3484,12 +4315,12 @@ function receiverIdOf(all, harness, id) {
3484
4315
 
3485
4316
  // src/control/local.ts
3486
4317
  import net from "node:net";
3487
- import os2 from "node:os";
3488
- import path14 from "node:path";
4318
+ import os3 from "node:os";
4319
+ import path15 from "node:path";
3489
4320
  function socketPath(state, env = process.env) {
3490
4321
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3491
- if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
3492
- return path14.join(state, "control.sock");
4322
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os3.userInfo().username}`;
4323
+ return path15.join(state, "control.sock");
3493
4324
  }
3494
4325
  function ask(sock, req, timeoutMs) {
3495
4326
  return new Promise((resolve) => {
@@ -3521,10 +4352,85 @@ function ask(sock, req, timeoutMs) {
3521
4352
  });
3522
4353
  }
3523
4354
 
4355
+ // src/fold.ts
4356
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
4357
+ import path16 from "node:path";
4358
+ var TOOLS_MODES = ["all", "main", "beat"];
4359
+ var TOOL_FOLD_MS = 6e4;
4360
+ var KEEP_MS = 24 * 60 * 6e4;
4361
+ function isToolsMode(v) {
4362
+ return typeof v === "string" && TOOLS_MODES.includes(v);
4363
+ }
4364
+ function toolsMode(s) {
4365
+ if (isToolsMode(s.tools)) return s.tools;
4366
+ if (isToolsMode(s.receiver_tools)) return s.receiver_tools;
4367
+ return "all";
4368
+ }
4369
+ var agentOf = (e) => typeof e.data.agent_id === "string" ? e.data.agent_id : "";
4370
+ var thins = (mode, agent) => mode === "beat" || mode === "main" && agent !== "";
4371
+ var askKey = (key, agent) => `${key}\0ask\0${agent}`;
4372
+ function sendTool(e, mode, clock, key) {
4373
+ if (mode === "all") return true;
4374
+ const agent = agentOf(e);
4375
+ const t = Date.parse(e.time);
4376
+ if (e.type === "attention.needed") {
4377
+ if (thins(mode, agent)) clock[askKey(key, agent)] = Number.isFinite(t) ? t : 0;
4378
+ return true;
4379
+ }
4380
+ if (e.type !== "tool.started" && e.type !== "tool.ended" || !thins(mode, agent)) return true;
4381
+ if (e.type === "tool.started") return false;
4382
+ if (!Number.isFinite(t)) return true;
4383
+ const ask2 = askKey(key, agent);
4384
+ const asked = clock[ask2];
4385
+ if (asked !== void 0 && t >= asked) {
4386
+ delete clock[ask2];
4387
+ clock[key] = t;
4388
+ return true;
4389
+ }
4390
+ const last = clock[key];
4391
+ if (last !== void 0 && t > last - TOOL_FOLD_MS && t < last + TOOL_FOLD_MS) return false;
4392
+ clock[key] = t;
4393
+ return true;
4394
+ }
4395
+ var ToolClock = class {
4396
+ constructor(root, sink) {
4397
+ this.root = root;
4398
+ this.sink = sink;
4399
+ }
4400
+ root;
4401
+ sink;
4402
+ file() {
4403
+ return path16.join(this.root, "folds", `${safeId(this.sink)}.json`);
4404
+ }
4405
+ read() {
4406
+ try {
4407
+ const j = JSON.parse(readFileSync11(this.file(), "utf8"));
4408
+ return j && typeof j === "object" && !Array.isArray(j) ? j : {};
4409
+ } catch {
4410
+ return {};
4411
+ }
4412
+ }
4413
+ write(c) {
4414
+ const newest = Math.max(0, ...Object.values(c).filter(Number.isFinite));
4415
+ const kept = {};
4416
+ for (const [k, v] of Object.entries(c)) if (Number.isFinite(v) && v >= newest - KEEP_MS) kept[k] = v;
4417
+ const f = this.file();
4418
+ mkdirSync6(path16.dirname(f), { recursive: true, mode: 448 });
4419
+ const tmp = `${f}.${process.pid}.tmp`;
4420
+ writeFileSync5(tmp, JSON.stringify(kept), { mode: 384 });
4421
+ renameSync3(tmp, f);
4422
+ }
4423
+ };
4424
+
3524
4425
  // src/run.ts
3525
4426
  async function tellDaemon(state, job, session, cwd, transcript) {
3526
4427
  const sock = socketPath(state);
3527
4428
  if (process.platform !== "win32" && !existsSync10(sock)) return;
4429
+ let mode = null;
4430
+ try {
4431
+ mode = claude_code_control_exports.hookMode(JSON.parse(job.stdin));
4432
+ } catch {
4433
+ }
3528
4434
  await ask(
3529
4435
  sock,
3530
4436
  {
@@ -3532,31 +4438,32 @@ async function tellDaemon(state, job, session, cwd, transcript) {
3532
4438
  session: `${job.harness}:${session}`,
3533
4439
  event: job.event,
3534
4440
  ...cwd ? { cwd } : {},
3535
- ...transcript ? { transcript } : {}
4441
+ ...transcript ? { transcript } : {},
4442
+ ...mode ? { mode } : {}
3536
4443
  },
3537
4444
  300
3538
4445
  );
3539
4446
  }
3540
4447
  function enqueueJob(state, job) {
3541
4448
  const jobs = jobsDir(state);
3542
- mkdirSync8(jobs, { recursive: true, mode: 448 });
3543
- const f = path15.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3544
- writeFileSync8(f, JSON.stringify(job), { mode: 384 });
4449
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
4450
+ const f = path17.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4451
+ writeFileSync6(f, JSON.stringify(job), { mode: 384 });
3545
4452
  return f;
3546
4453
  }
3547
4454
  function releaseJob(claimed) {
3548
4455
  if (!claimed) return;
3549
4456
  try {
3550
- unlinkSync4(claimed);
4457
+ unlinkSync3(claimed);
3551
4458
  } catch {
3552
4459
  }
3553
4460
  }
3554
4461
  function log(state, line) {
3555
4462
  try {
3556
- mkdirSync8(state, { recursive: true, mode: 448 });
3557
- const f = path15.join(state, "log");
4463
+ mkdirSync7(state, { recursive: true, mode: 448 });
4464
+ const f = path17.join(state, "log");
3558
4465
  try {
3559
- if (statSync6(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
4466
+ if (statSync7(f).size > 1024 * 1024) renameSync4(f, `${f}.1`);
3560
4467
  } catch {
3561
4468
  }
3562
4469
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3565,11 +4472,11 @@ function log(state, line) {
3565
4472
  }
3566
4473
  }
3567
4474
  function factsState(state) {
3568
- const file = (h, s) => path15.join(state, "facts", safeId(h), `${safeId(s)}.json`);
4475
+ const file = (h, s) => path17.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3569
4476
  return {
3570
4477
  get(h, s) {
3571
4478
  try {
3572
- return JSON.parse(readFileSync10(file(h, s), "utf8"));
4479
+ return JSON.parse(readFileSync12(file(h, s), "utf8"));
3573
4480
  } catch {
3574
4481
  return {};
3575
4482
  }
@@ -3578,8 +4485,8 @@ function factsState(state) {
3578
4485
  const cur = this.get(h, s);
3579
4486
  const next = { ...cur, ...patch };
3580
4487
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
3581
- mkdirSync8(path15.dirname(file(h, s)), { recursive: true, mode: 448 });
3582
- writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
4488
+ mkdirSync7(path17.dirname(file(h, s)), { recursive: true, mode: 448 });
4489
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
3583
4490
  }
3584
4491
  };
3585
4492
  }
@@ -3592,10 +4499,10 @@ function buildSinks(sinks, o = {}) {
3592
4499
  o.locked?.(raw.name);
3593
4500
  continue;
3594
4501
  }
3595
- const tier = Math.min(s.tier, s.max_tier ?? 3);
4502
+ const tier = Math.trunc(Math.min(Number(s.tier) || 0, s.max_tier ?? 3));
3596
4503
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
3597
4504
  else if (s.url.startsWith("file:"))
3598
- out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os3.homedir()), tier, !!s.pii));
4505
+ out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os4.homedir()), tier, !!s.pii));
3599
4506
  else if (/^https?:\/\//.test(s.url)) out.push(new HttpSink(s, tier));
3600
4507
  }
3601
4508
  return out;
@@ -3603,13 +4510,14 @@ function buildSinks(sinks, o = {}) {
3603
4510
  var PROGRESS_MS = 2e4;
3604
4511
  function openTurn(read, o) {
3605
4512
  const last = read.turns[read.turns.length - 1];
3606
- const fresh = (user, at) => ({
4513
+ const fresh = (user, at, injected) => ({
3607
4514
  turn: o.base + read.turns.length,
3608
4515
  user: user.slice(0, MAX_TURN_SIDE),
3609
4516
  assistant: "",
3610
- at
4517
+ at,
4518
+ ...injected ? { injected: true } : {}
3611
4519
  });
3612
- if (read.open) return fresh(read.open.user, read.open.at);
4520
+ if (read.open) return fresh(read.open.user, read.open.at, read.open.injected);
3613
4521
  const prompt = o.prompt?.trim();
3614
4522
  if (prompt && !isInjected(prompt) && !(last && last.user.includes(prompt.slice(0, 200)))) return fresh(prompt, o.at);
3615
4523
  if (!last) return null;
@@ -3617,6 +4525,7 @@ function openTurn(read, o) {
3617
4525
  turn: o.base + read.turns.length - 1,
3618
4526
  user: last.user.slice(0, MAX_TURN_SIDE),
3619
4527
  assistant: capTail(last.assistant, MAX_TURN_SIDE),
4528
+ ...last.injected ? { injected: true } : {},
3620
4529
  at: last.at
3621
4530
  };
3622
4531
  }
@@ -3624,6 +4533,7 @@ var MAX_TURN_SIDE = 2e4;
3624
4533
  var JOB_OWN_ENV = [
3625
4534
  "CLAUDE_CODE_SESSION_ATTENDED",
3626
4535
  "CLAUDE_CODE_HOST_SESSION_ID",
4536
+ "SESSIONPIPE_ASKED_BY",
3627
4537
  "SESSIONPIPE_CONTROL_JOB",
3628
4538
  "SESSIONPIPE_LAUNCHED_BY"
3629
4539
  ];
@@ -3651,7 +4561,7 @@ async function runJob(job, opts = {}) {
3651
4561
  );
3652
4562
  return { events: [], delivered: {} };
3653
4563
  }
3654
- const id = receiverIdOf(readAliases(aliasFile(path15.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
4564
+ const id = receiverIdOf(readAliases(aliasFile(path17.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3655
4565
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
3656
4566
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
3657
4567
  const outbox = new Outbox(state);
@@ -3688,7 +4598,7 @@ async function runJob(job, opts = {}) {
3688
4598
  learned = rest;
3689
4599
  if (harness_version) facts.save(job.harness, id, { harness_version });
3690
4600
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
3691
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path15.isAbsolute(r.session.cwd))
4601
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path17.isAbsolute(r.session.cwd))
3692
4602
  facts.save(job.harness, id, { cwd: r.session.cwd });
3693
4603
  }
3694
4604
  const known = mem.session ?? {};
@@ -3701,10 +4611,11 @@ async function runJob(job, opts = {}) {
3701
4611
  ...learned.branch ? { branch: learned.branch } : {},
3702
4612
  id,
3703
4613
  seq: 0,
3704
- machine: machineName(cfg, os3.hostname()),
4614
+ machine: machineName(cfg, os4.hostname()),
3705
4615
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
3706
4616
  ...r.session.model ? { model: r.session.model } : {},
3707
- ...r.session.launched_by ? { launched_by: r.session.launched_by } : {}
4617
+ ...r.session.launched_by ? { launched_by: r.session.launched_by } : {},
4618
+ ...r.session.delegated_by ? { delegated_by: r.session.delegated_by } : {}
3708
4619
  };
3709
4620
  const version = facts.get(job.harness, id).harness_version ?? void 0;
3710
4621
  for (const a of r.events) {
@@ -3758,7 +4669,13 @@ async function runJob(job, opts = {}) {
3758
4669
  {
3759
4670
  type: "turn.transcript",
3760
4671
  harnessEvent: job.event,
3761
- data: { turn: turnNo++, user: t.user, assistant: t.assistant, at: new Date(t.at).toISOString() }
4672
+ data: {
4673
+ turn: turnNo++,
4674
+ user: t.user,
4675
+ assistant: t.assistant,
4676
+ at: new Date(t.at).toISOString(),
4677
+ ...t.injected ? { injected: true } : {}
4678
+ }
3762
4679
  },
3763
4680
  {
3764
4681
  harness: { name: job.harness, ...version ? { version } : {} },
@@ -3799,29 +4716,83 @@ async function runJob(job, opts = {}) {
3799
4716
  } finally {
3800
4717
  release();
3801
4718
  }
3802
- const delivered = await drain(state, cfg.sinks, outbox, ref) ?? {};
3803
4719
  if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
4720
+ const delivered = opts.drain === false ? {} : await drain(state, cfg.sinks, outbox, ref, {
4721
+ ...opts.clock ? { clock: opts.clock } : {},
4722
+ ...opts.deadline !== void 0 ? { deadline: opts.deadline } : {},
4723
+ ...opts.handoff ? { handoff: opts.handoff } : {}
4724
+ }) ?? {};
3804
4725
  return { events, delivered };
3805
4726
  }
3806
- async function drain(state, sinkConfigs, outbox, first) {
3807
- const more = path15.join(state, "locks", "flush.more");
4727
+ var REAL_CLOCK = { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
4728
+ var PACE_MS = 1e3;
4729
+ var sentFile = (state) => path17.join(state, "locks", "flush.sent");
4730
+ function lastSent(state) {
4731
+ try {
4732
+ return Number(readFileSync12(sentFile(state), "utf8"));
4733
+ } catch {
4734
+ return 0;
4735
+ }
4736
+ }
4737
+ function noteSent(state, at) {
4738
+ try {
4739
+ mkdirSync7(path17.dirname(sentFile(state)), { recursive: true, mode: 448 });
4740
+ writeFileSync6(sentFile(state), String(at), { mode: 384 });
4741
+ } catch {
4742
+ }
4743
+ }
4744
+ function waitingForReceiver(state, sinkConfigs, outbox, now) {
4745
+ const receivers = sinkConfigs.filter(
4746
+ (s) => !s.paused && /^https?:\/\//.test(s.url) && !((readBackoff(state, s.name)?.until ?? 0) > now)
4747
+ );
4748
+ if (!receivers.length) return false;
4749
+ const refs = outbox.sessions();
4750
+ for (const s of receivers) {
4751
+ const cursors = new Cursors(state, s.name);
4752
+ const c = cursors.read();
4753
+ for (const ref of refs) {
4754
+ try {
4755
+ if (statSync7(outbox.file(ref)).size > (c[cursors.key(ref)] ?? 0)) return true;
4756
+ } catch {
4757
+ }
4758
+ }
4759
+ }
4760
+ return false;
4761
+ }
4762
+ var PASS_BUDGET_MS = 3e3;
4763
+ var moreFile = (state) => path17.join(state, "locks", "flush.more");
4764
+ function markMore(state) {
4765
+ try {
4766
+ mkdirSync7(path17.dirname(moreFile(state)), { recursive: true, mode: 448 });
4767
+ writeFileSync6(moreFile(state), "", { mode: 384 });
4768
+ } catch {
4769
+ }
4770
+ }
4771
+ async function drain(state, sinkConfigs, outbox, first, o = {}) {
4772
+ const clock = o.clock ?? REAL_CLOCK;
4773
+ const more = moreFile(state);
3808
4774
  let release = takeNamedLock(state, "flush");
3809
4775
  if (!release) {
3810
- try {
3811
- writeFileSync8(more, "", { mode: 384 });
3812
- } catch {
3813
- }
4776
+ markMore(state);
3814
4777
  release = takeNamedLock(state, "flush");
3815
4778
  if (!release) return null;
3816
4779
  }
3817
4780
  const delivered = {};
3818
4781
  for (; ; ) {
3819
- try {
3820
- unlinkSync4(more);
3821
- } catch {
4782
+ if (o.deadline !== void 0 && clock.now() + PASS_BUDGET_MS > o.deadline) {
4783
+ markMore(state);
4784
+ release();
4785
+ o.handoff?.();
4786
+ return delivered;
3822
4787
  }
3823
4788
  try {
3824
- for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first)))
4789
+ const wait = Math.min(PACE_MS, lastSent(state) + PACE_MS - clock.now());
4790
+ if (wait > 0 && waitingForReceiver(state, sinkConfigs, outbox, clock.now())) await clock.sleep(wait);
4791
+ try {
4792
+ unlinkSync3(more);
4793
+ } catch {
4794
+ }
4795
+ for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first, { clock })))
3825
4796
  delivered[k] = (delivered[k] ?? 0) + v;
3826
4797
  } finally {
3827
4798
  release();
@@ -3832,10 +4803,10 @@ async function drain(state, sinkConfigs, outbox, first) {
3832
4803
  }
3833
4804
  }
3834
4805
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
3835
- var backoffFile = (state, sink) => path15.join(state, "backoff", `${safeId(sink)}.json`);
4806
+ var backoffFile = (state, sink) => path17.join(state, "backoff", `${safeId(sink)}.json`);
3836
4807
  function readBackoff(state, sink) {
3837
4808
  try {
3838
- const b = JSON.parse(readFileSync10(backoffFile(state, sink), "utf8"));
4809
+ const b = JSON.parse(readFileSync12(backoffFile(state, sink), "utf8"));
3839
4810
  return typeof b.until === "number" ? b : null;
3840
4811
  } catch {
3841
4812
  return null;
@@ -3848,15 +4819,15 @@ function noteFailure(state, sink, res, prev) {
3848
4819
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
3849
4820
  const f = backoffFile(state, sink);
3850
4821
  try {
3851
- mkdirSync8(path15.dirname(f), { recursive: true, mode: 448 });
3852
- writeFileSync8(f, JSON.stringify(b), { mode: 384 });
4822
+ mkdirSync7(path17.dirname(f), { recursive: true, mode: 448 });
4823
+ writeFileSync6(f, JSON.stringify(b), { mode: 384 });
3853
4824
  } catch {
3854
4825
  }
3855
4826
  return b;
3856
4827
  }
3857
4828
  function clearBackoff(state, sink) {
3858
4829
  try {
3859
- unlinkSync4(backoffFile(state, sink));
4830
+ unlinkSync3(backoffFile(state, sink));
3860
4831
  } catch {
3861
4832
  }
3862
4833
  }
@@ -3866,14 +4837,31 @@ var BATCH = 50;
3866
4837
  var MAX_PER_RUN = 500;
3867
4838
  async function flush(state, sinkConfigs, outbox, first, o = {}) {
3868
4839
  const delivered = {};
4840
+ const clock = o.clock ?? REAL_CLOCK;
3869
4841
  const sinks = buildSinks(sinkConfigs, {
3870
4842
  locked: (name) => log(state, `sink ${name}: its token is in a keychain this process can't read; left for later`)
3871
4843
  });
4844
+ const modes = new Map(sinkConfigs.map((s) => [s.name, toolsMode(s)]));
3872
4845
  const refs = outbox.sessions();
3873
4846
  if (first) {
3874
4847
  const k = `${first.harness}/${safeId(first.session)}`;
3875
4848
  refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
3876
4849
  }
4850
+ const unreadable = /* @__PURE__ */ new Set();
4851
+ const readOrSkip = (ref, offset, room) => {
4852
+ try {
4853
+ return outbox.read(ref, offset, room);
4854
+ } catch (e) {
4855
+ const code = e.code;
4856
+ if (typeof code !== "string") throw e;
4857
+ const f = outbox.file(ref);
4858
+ if (!unreadable.has(f)) {
4859
+ unreadable.add(f);
4860
+ log(state, `drain: can't read ${f} (${code}); skipped, its cursors stay`);
4861
+ }
4862
+ return null;
4863
+ }
4864
+ };
3877
4865
  const drainSink = async (sink) => {
3878
4866
  let backoff = readBackoff(state, sink.name);
3879
4867
  if (backoff && !o.force && backoff.until > Date.now()) {
@@ -3882,11 +4870,16 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3882
4870
  }
3883
4871
  const cursors = new Cursors(state, sink.name);
3884
4872
  const c = cursors.read();
4873
+ const mode = modes.get(sink.name);
4874
+ const tools = new ToolClock(state, sink.name);
4875
+ let clockNow = mode === "all" ? {} : tools.read();
4876
+ let clockNext = { ...clockNow };
3885
4877
  let sent = 0;
3886
4878
  let stalled = false;
3887
4879
  let batch = [];
3888
4880
  let pending = {};
3889
4881
  const deliver = async (events) => {
4882
+ if (sink instanceof HttpSink) noteSent(state, clock.now());
3890
4883
  const res = await sink.send(events);
3891
4884
  if (res.ok || res.action !== "split") return res;
3892
4885
  if (events.length === 1) {
@@ -3894,10 +4887,20 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3894
4887
  log(state, `sink ${sink.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
3895
4888
  return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
3896
4889
  }
4890
+ const part = async (some) => {
4891
+ const r = await deliver(some);
4892
+ if (r.ok || r.action !== "drop") return r;
4893
+ log(
4894
+ state,
4895
+ `sink ${sink.name}: ${r.status ?? ""} ${r.error ?? ""} \u2192 drop, ${some.length} of ${events.length} events`
4896
+ );
4897
+ const rejected = some.map((e) => ({ id: e.id, reason: "dropped" }));
4898
+ return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected };
4899
+ };
3897
4900
  const half = Math.ceil(events.length / 2);
3898
- const a = await deliver(events.slice(0, half));
4901
+ const a = await part(events.slice(0, half));
3899
4902
  if (!a.ok) return a;
3900
- const b = await deliver(events.slice(half));
4903
+ const b = await part(events.slice(half));
3901
4904
  if (!b.ok) return b;
3902
4905
  return {
3903
4906
  ok: true,
@@ -3907,11 +4910,17 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3907
4910
  rejected: [...a.rejected ?? [], ...b.rejected ?? []]
3908
4911
  };
3909
4912
  };
4913
+ const commitClock = () => {
4914
+ if (mode === "all" || JSON.stringify(clockNext) === JSON.stringify(clockNow)) return;
4915
+ tools.write(clockNext);
4916
+ clockNow = { ...clockNext };
4917
+ };
3910
4918
  const send = async () => {
3911
4919
  if (!batch.length) {
3912
4920
  Object.assign(c, pending);
3913
4921
  pending = {};
3914
4922
  cursors.write(c);
4923
+ commitClock();
3915
4924
  return true;
3916
4925
  }
3917
4926
  const res = await deliver(batch);
@@ -3927,6 +4936,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3927
4936
  if (res.action !== "drop") {
3928
4937
  batch = [];
3929
4938
  pending = {};
4939
+ clockNext = { ...clockNow };
3930
4940
  return false;
3931
4941
  }
3932
4942
  } else if (backoff) {
@@ -3936,6 +4946,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3936
4946
  sent += batch.length;
3937
4947
  Object.assign(c, pending);
3938
4948
  cursors.write(c);
4949
+ commitClock();
3939
4950
  batch = [];
3940
4951
  pending = {};
3941
4952
  return true;
@@ -3945,11 +4956,15 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3945
4956
  let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
3946
4957
  for (; ; ) {
3947
4958
  const room = BATCH - batch.length;
3948
- const { events, offset: next } = outbox.read(ref, offset, room);
4959
+ const read = readOrSkip(ref, offset, room);
4960
+ if (!read) break;
4961
+ const { events, offset: next } = read;
3949
4962
  if (!events.length) break;
3950
4963
  for (const e of events) {
3951
- const f = filterEvent(e, sink.tier, sink.pii, { home: os3.homedir() });
3952
- if (f) batch.push(f);
4964
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os4.homedir() });
4965
+ if (!f) continue;
4966
+ if (sendTool(f, mode, clockNext, cursors.key(ref))) batch.push(f);
4967
+ else if (o.thinned) o.thinned[sink.name] = (o.thinned[sink.name] ?? 0) + 1;
3953
4968
  }
3954
4969
  offset = next;
3955
4970
  pending[cursors.key(ref)] = offset;
@@ -3981,7 +4996,7 @@ function applySinkVerdict(name, res) {
3981
4996
  } catch {
3982
4997
  }
3983
4998
  }
3984
- var jobsDir = (state) => path15.join(state, "jobs");
4999
+ var jobsDir = (state) => path17.join(state, "jobs");
3985
5000
  export {
3986
5001
  HttpSink,
3987
5002
  buildSinks,