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/worker.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.12.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.13.1 · Apache-2.0 · https://sessionpipe.org
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
3
8
 
4
9
  // src/worker.ts
5
- import { spawn } from "node:child_process";
10
+ import { spawn as spawn2 } from "node:child_process";
6
11
  import { realpathSync as realpathSync2 } from "node:fs";
7
12
  import { pathToFileURL } from "node:url";
8
13
 
@@ -32,7 +37,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
32
37
  // ../core/src/adapters/claude-code.ts
33
38
  import { execFileSync as execFileSync2 } from "node:child_process";
34
39
  import { createHash } from "node:crypto";
35
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
40
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
36
41
  import { userInfo } from "node:os";
37
42
  import path4 from "node:path";
38
43
 
@@ -184,7 +189,14 @@ ${m.text}`;
184
189
  continue;
185
190
  }
186
191
  if (cur) turns.push(cur);
187
- cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
192
+ cur = {
193
+ user: m.text,
194
+ assistant: "",
195
+ at: m.at,
196
+ startLine: m.line,
197
+ endLine: m.line,
198
+ ...m.injected ? { injected: true } : {}
199
+ };
188
200
  } else {
189
201
  if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
190
202
  cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
@@ -194,10 +206,17 @@ ${m.text}`;
194
206
  if (cur?.assistant) turns.push(cur);
195
207
  const line = cur && !cur.assistant ? cur.startLine : endLine;
196
208
  const out = {
197
- turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
209
+ turns: turns.map((t) => ({
210
+ user: t.user,
211
+ assistant: t.assistant,
212
+ at: t.at,
213
+ endLine: t.endLine,
214
+ ...t.injected ? { injected: true } : {}
215
+ })),
198
216
  line
199
217
  };
200
- if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
218
+ if (cur && !cur.assistant && cur.user)
219
+ out.open = { user: cur.user, at: cur.at, ...cur.injected ? { injected: true } : {} };
201
220
  return out;
202
221
  }
203
222
  var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
@@ -220,27 +239,51 @@ var textParts = (c, kinds) => {
220
239
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
221
240
  ).map((b) => b.text).join("\n");
222
241
  };
242
+ var SPOKEN = /* @__PURE__ */ new Set(["human", "peer"]);
243
+ var PEER_INTRO = /sent a message[^\n]*:$/;
244
+ var PEER_OUTRO = "\n\nThis came from another ";
245
+ var ENVELOPE = /^<cross-session-message\b[^>]*>\n([\s\S]*)\n<\/cross-session-message>$/;
246
+ function peerAsk(origin, text) {
247
+ const body = origin?.body;
248
+ let t = typeof body === "string" && body.trim() ? body : text;
249
+ if (t !== body) {
250
+ const nl = t.indexOf("\n");
251
+ if (nl > 0 && PEER_INTRO.test(t.slice(0, nl))) t = t.slice(nl + 1);
252
+ const outro = t.indexOf(PEER_OUTRO);
253
+ if (outro > 0) t = t.slice(0, outro);
254
+ }
255
+ return (ENVELOPE.exec(t.trim())?.[1] ?? t).trim();
256
+ }
223
257
  var queuedPrompt = (a) => {
224
258
  if (!a || typeof a !== "object") return "";
225
259
  const q = a;
226
260
  if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
227
- if (q.origin?.kind && q.origin.kind !== "human") return "";
228
- return textParts(q.prompt, ["text"]).trim();
261
+ const kind = q.origin?.kind ?? "human";
262
+ if (!SPOKEN.has(kind)) return "";
263
+ const text = textParts(q.prompt, ["text"]).trim();
264
+ return kind === "peer" ? peerAsk(q.origin, text) : text;
229
265
  };
266
+ var channelText = (text) => /^<channel\b[^>]*>([\s\S]*)<\/channel>$/.exec(text)?.[1]?.trim() ?? "";
230
267
  function readClaude(lines, from) {
231
268
  const msgs = [];
232
269
  for (let i = from; i < lines.length; i++) {
233
270
  const j = lines[i] ? parse(lines[i]) : null;
234
271
  if (!j || j.isSidechain) continue;
235
272
  const origin = j.origin;
236
- if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
237
- continue;
273
+ const channel = j.type === "user" && origin?.kind === "channel";
274
+ const peer = origin?.kind === "peer";
275
+ if (j.type === "user" && (j.isCompactSummary || j.repeatsCompactSummary)) continue;
276
+ if (j.type === "user" && !channel && !peer && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
238
277
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
239
278
  const c = j.message?.content;
240
- if (j.type === "user") {
241
- const text = textParts(c, ["text"]).trim();
279
+ if (channel) {
280
+ const text = channelText(textParts(c, ["text"]).trim());
281
+ if (text) msgs.push({ role: "user", text, at, line: i, injected: true });
282
+ } else if (j.type === "user") {
283
+ const raw = textParts(c, ["text"]).trim();
284
+ const text = peer ? peerAsk(origin, raw) : raw;
242
285
  if (!text || isInjected(text)) continue;
243
- msgs.push({ role: "user", text, at, line: i });
286
+ msgs.push({ role: "user", text, at, line: i, ...peer ? { injected: true } : {} });
244
287
  } else if (j.type === "assistant" && Array.isArray(c)) {
245
288
  const text = textParts(c, ["text"]).trim();
246
289
  if (text) msgs.push({ role: "assistant", text, at, line: i });
@@ -352,6 +395,41 @@ function readJson(file) {
352
395
  const f = readJsonFile(file);
353
396
  return f.ok ? f.value : {};
354
397
  }
398
+ function refused(file, e, what, existed) {
399
+ const { code, syscall } = e;
400
+ const why = `can't ${what} it (${code ?? String(e)})`;
401
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
402
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
403
+ return {
404
+ file,
405
+ changed: true,
406
+ skipped: true,
407
+ ...existed ? {} : { created: true },
408
+ note: `${why} part way; check the file before the harness reads it`
409
+ };
410
+ }
411
+ function writeBack(file, text, existed = true) {
412
+ try {
413
+ mkdirSync(path3.dirname(file), { recursive: true });
414
+ writeFileSync(file, text);
415
+ return null;
416
+ } catch (e) {
417
+ return refused(file, e, "write", existed);
418
+ }
419
+ }
420
+ function removeEmptied(file, empty) {
421
+ try {
422
+ unlinkSync(file);
423
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
424
+ } catch (e) {
425
+ const bad = refused(file, e, "remove", true);
426
+ return writeBack(file, empty) ?? {
427
+ file,
428
+ changed: true,
429
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
430
+ };
431
+ }
432
+ }
355
433
  function entryFor(event, harness, cmd, o) {
356
434
  const matcher = o.matcherFor(event);
357
435
  return {
@@ -393,8 +471,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
393
471
  }
394
472
  if (changed) {
395
473
  settings2.hooks = hooks;
396
- mkdirSync(path3.dirname(file), { recursive: true });
397
- writeFileSync(file, formatJson(settings2, f));
474
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
475
+ if (bad) return bad;
398
476
  }
399
477
  return { file, changed, created: !f.existed && changed };
400
478
  }
@@ -417,12 +495,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
417
495
  }
418
496
  if (!changed) return { file, changed: false };
419
497
  if (!Object.keys(hooks).length) delete settings2.hooks;
420
- if (opts.created && !Object.keys(settings2).length) {
421
- unlinkSync(file);
422
- return { file, changed: true, note: "removed (sessionpipe created it)" };
423
- }
424
- writeFileSync(file, formatJson(settings2, f));
425
- return { file, changed: true };
498
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
499
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
426
500
  }
427
501
  function installedClaudeShaped(file, harness, events, cmd, o) {
428
502
  const f = readJsonFile(file);
@@ -501,7 +575,7 @@ var claudeCode = {
501
575
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
502
576
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
503
577
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
504
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
578
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
505
579
  return line ? [hooks, line] : [hooks];
506
580
  }),
507
581
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -509,7 +583,9 @@ var claudeCode = {
509
583
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
510
584
  created: opts.created?.includes(settingsIn(d))
511
585
  });
512
- return line && !hooks.changed ? line : hooks;
586
+ if (line?.changed && !line.skipped && hooks.skipped)
587
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
588
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
513
589
  }),
514
590
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
515
591
  file: settingsIn(d),
@@ -604,7 +680,8 @@ var claudeCode = {
604
680
  attention_id: call_id ?? permissionId(),
605
681
  kind: "permission",
606
682
  tool,
607
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
683
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
684
+ agent_id
608
685
  })
609
686
  );
610
687
  break;
@@ -613,7 +690,8 @@ var claudeCode = {
613
690
  ev("attention.needed", {
614
691
  attention_id: `notif-${Date.now()}`,
615
692
  kind: notifKind(s.notification_type),
616
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
693
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
694
+ agent_id
617
695
  })
618
696
  );
619
697
  break;
@@ -738,8 +816,11 @@ function statusLine(file, want, cmd) {
738
816
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
739
817
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
740
818
  else delete f.value.statusLine;
741
- writeFileSync2(file, formatJson(f.value, f));
742
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
819
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
820
+ file,
821
+ changed: true,
822
+ note: want ? "status line written (account limits)" : "status line removed"
823
+ };
743
824
  }
744
825
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
745
826
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1154,8 +1235,44 @@ async function claudeAiTitle(bridge, token) {
1154
1235
  }
1155
1236
 
1156
1237
  // ../core/src/adapters/headless.ts
1238
+ import { spawn } from "node:child_process";
1157
1239
  import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1158
1240
  import path5 from "node:path";
1241
+
1242
+ // ../core/src/launched-by.ts
1243
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
1244
+ var DELEGATED_BY_ENV = "SESSIONPIPE_ASKED_BY";
1245
+ var ASK_CHAIN_ENV = "SESSIONPIPE_ASK_CHAIN";
1246
+ var LINEAGE_ENV = [LAUNCHED_BY_ENV, DELEGATED_BY_ENV, ASK_CHAIN_ENV];
1247
+ var SESSION_REF = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
1248
+ function withoutLineage(env) {
1249
+ const e = { ...env };
1250
+ for (const k of LINEAGE_ENV) delete e[k];
1251
+ return e;
1252
+ }
1253
+ function refIn(env, name) {
1254
+ const v = env[name]?.trim();
1255
+ return v && SESSION_REF.test(v) ? v : void 0;
1256
+ }
1257
+ function launchedBy(env) {
1258
+ return refIn(env, LAUNCHED_BY_ENV);
1259
+ }
1260
+ function delegatedBy(env) {
1261
+ return refIn(env, DELEGATED_BY_ENV);
1262
+ }
1263
+ function readHook(adapter, input) {
1264
+ const r = adapter.fromHook(input);
1265
+ if (r && !r.session.parent_id) {
1266
+ const self = `${adapter.name}:${r.session.id}`;
1267
+ const launcher = launchedBy(input.env);
1268
+ if (launcher && launcher !== self) r.session.launched_by = launcher;
1269
+ const asker = delegatedBy(input.env);
1270
+ if (asker && asker !== self) r.session.delegated_by = asker;
1271
+ }
1272
+ return r;
1273
+ }
1274
+
1275
+ // ../core/src/adapters/headless.ts
1159
1276
  var JOB_TIMEOUT_MS = 30 * 6e4;
1160
1277
  var OUT_CAP = 1024 * 1024;
1161
1278
  function isExe(p) {
@@ -1197,22 +1314,119 @@ function spawnable(bin, args) {
1197
1314
  const target = shimTarget(bin);
1198
1315
  return target ? { file: process.execPath, args: [target, ...args] } : null;
1199
1316
  }
1317
+ 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`;
1200
1318
  function probe(bin, args) {
1201
1319
  const s = spawnable(bin, args);
1202
1320
  return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1203
1321
  }
1322
+ function runHeadless(bin, args, o) {
1323
+ return new Promise((resolve) => {
1324
+ let out = [];
1325
+ let outLen = 0;
1326
+ let tooBig = false;
1327
+ let errTail = "";
1328
+ let timedOut = false;
1329
+ let settled = false;
1330
+ let child;
1331
+ const c = spawnable(bin, args);
1332
+ if (!c) {
1333
+ resolve({ error: shimNote(bin) });
1334
+ return;
1335
+ }
1336
+ try {
1337
+ child = spawn(c.file, c.args, {
1338
+ cwd: o.cwd,
1339
+ env: o.env,
1340
+ stdio: ["ignore", "pipe", "pipe"],
1341
+ detached: process.platform !== "win32",
1342
+ windowsHide: true
1343
+ });
1344
+ } catch (e) {
1345
+ resolve({ error: e.message });
1346
+ return;
1347
+ }
1348
+ const kill = (sig) => {
1349
+ try {
1350
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, sig);
1351
+ else child.kill(sig);
1352
+ } catch {
1353
+ try {
1354
+ child.kill(sig);
1355
+ } catch {
1356
+ }
1357
+ }
1358
+ };
1359
+ const keep = (line) => {
1360
+ if (!o.keep?.(line)) return;
1361
+ out = [Buffer.from(line)];
1362
+ outLen = line.length;
1363
+ };
1364
+ let partial = "";
1365
+ child.stdout?.on("data", (b) => {
1366
+ if (o.onLine) {
1367
+ const chunk = partial + b.toString("utf8");
1368
+ const lines = chunk.split("\n");
1369
+ partial = lines.pop();
1370
+ for (const line of lines) {
1371
+ if (!line.trim()) continue;
1372
+ try {
1373
+ o.onLine(line);
1374
+ } catch {
1375
+ }
1376
+ keep(line);
1377
+ }
1378
+ if (partial.length > OUT_CAP) partial = "";
1379
+ return;
1380
+ }
1381
+ if (tooBig) return;
1382
+ if (outLen + b.length > OUT_CAP) {
1383
+ tooBig = true;
1384
+ out = [];
1385
+ return;
1386
+ }
1387
+ out.push(b);
1388
+ outLen += b.length;
1389
+ });
1390
+ child.stderr?.on("data", (b) => {
1391
+ errTail = (errTail + b.toString("utf8")).slice(-4e3);
1392
+ });
1393
+ const timer = setTimeout(() => {
1394
+ timedOut = true;
1395
+ kill("SIGTERM");
1396
+ setTimeout(() => kill("SIGKILL"), 1e4).unref();
1397
+ }, o.timeoutMs ?? JOB_TIMEOUT_MS);
1398
+ const result = (code, signal) => ({
1399
+ code,
1400
+ signal,
1401
+ stdout: tooBig ? null : Buffer.concat(out).toString("utf8"),
1402
+ tooBig,
1403
+ stderr: errTail,
1404
+ timedOut
1405
+ });
1406
+ const finish = (r) => {
1407
+ if (settled) return;
1408
+ settled = true;
1409
+ clearTimeout(timer);
1410
+ resolve(r);
1411
+ };
1412
+ child.on("error", (e) => finish({ error: e.message }));
1413
+ child.on("exit", (code, signal) => setTimeout(() => finish(result(code, signal)), 2e3).unref());
1414
+ child.on("close", (code, signal) => {
1415
+ if (o.onLine && partial.trim()) {
1416
+ try {
1417
+ o.onLine(partial);
1418
+ } catch {
1419
+ }
1420
+ keep(partial);
1421
+ partial = "";
1422
+ }
1423
+ finish(result(code, signal));
1424
+ });
1425
+ });
1426
+ }
1204
1427
 
1205
1428
  // ../core/src/adapters/antigravity.ts
1206
- import {
1207
- existsSync as existsSync3,
1208
- mkdirSync as mkdirSync2,
1209
- readdirSync as readdirSync3,
1210
- readFileSync as readFileSync4,
1211
- realpathSync,
1212
- statSync as statSync4,
1213
- unlinkSync as unlinkSync2,
1214
- writeFileSync as writeFileSync3
1215
- } from "node:fs";
1429
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync4 } from "node:fs";
1216
1430
  import path6 from "node:path";
1217
1431
  var NAME2 = "antigravity";
1218
1432
  var HOOK_NAME = "sessionpipe";
@@ -1244,10 +1458,8 @@ var antigravity = {
1244
1458
  const e = entry(cmd);
1245
1459
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1246
1460
  hooks[HOOK_NAME] = e;
1247
- mkdirSync2(path6.dirname(file), { recursive: true });
1248
- writeFileSync3(file, formatJson(hooks, f));
1249
1461
  return [
1250
- {
1462
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
1251
1463
  file,
1252
1464
  changed: true,
1253
1465
  created: !f.existed,
@@ -1263,12 +1475,8 @@ var antigravity = {
1263
1475
  const hooks = f.value;
1264
1476
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1265
1477
  delete hooks[HOOK_NAME];
1266
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1267
- unlinkSync2(file);
1268
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1269
- }
1270
- writeFileSync3(file, formatJson(hooks, f));
1271
- return [{ file, changed: true }];
1478
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
1479
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
1272
1480
  },
1273
1481
  installed(cmd, env = process.env) {
1274
1482
  const file = hooksFile(env);
@@ -1490,6 +1698,50 @@ function antigravityFacts(file, opts) {
1490
1698
  return out;
1491
1699
  }
1492
1700
 
1701
+ // ../core/src/adapters/claude-code-control.ts
1702
+ var claude_code_control_exports = {};
1703
+ __export(claude_code_control_exports, {
1704
+ CLAUDE_MODES: () => CLAUDE_MODES,
1705
+ JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
1706
+ MODE_RANK: () => MODE_RANK,
1707
+ NO_HOME_SETTINGS: () => NO_HOME_SETTINGS,
1708
+ ceilingLevel: () => ceilingLevel,
1709
+ claudeArgs: () => claudeArgs,
1710
+ claudeEnv: () => claudeEnv,
1711
+ claudeLogin: () => claudeLogin2,
1712
+ cmpVersion: () => cmpVersion,
1713
+ defaultLevel: () => defaultLevel,
1714
+ detectCaps: () => detectCaps,
1715
+ findClaude: () => findClaude,
1716
+ findTranscript: () => findTranscript,
1717
+ findTranscripts: () => findTranscripts,
1718
+ homeSettings: () => homeSettings,
1719
+ hookMode: () => hookMode,
1720
+ isUuid: () => isUuid,
1721
+ liveProcess: () => liveProcess,
1722
+ liveRecord: () => liveRecord,
1723
+ liveRecords: () => liveRecords,
1724
+ loginCommand: () => loginCommand,
1725
+ loginHint: () => loginHint,
1726
+ modeFlags: () => modeFlags,
1727
+ modeRank: () => modeRank,
1728
+ offeredLevels: () => offeredLevels,
1729
+ outputArgs: () => outputArgs,
1730
+ parseAuthStatus: () => parseAuthStatus,
1731
+ parseCaps: () => parseCaps,
1732
+ parseResult: () => parseResult,
1733
+ peerClass: () => peerClass,
1734
+ recentlyWritten: () => recentlyWritten,
1735
+ relaunchMode: () => relaunchMode,
1736
+ resolveHome: () => resolveHome,
1737
+ runClaude: () => runClaude,
1738
+ sessionFolder: () => sessionFolder
1739
+ });
1740
+ import { execFile, execFileSync as execFileSync3 } from "node:child_process";
1741
+ import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, readSync as readSync2, statSync as statSync5 } from "node:fs";
1742
+ import os2 from "node:os";
1743
+ import path7 from "node:path";
1744
+
1493
1745
  // ../core/src/control/verify.ts
1494
1746
  var GRANT_MAX_MS = 24 * 36e5;
1495
1747
  var SKEW_MS = 5 * 6e4;
@@ -1500,10 +1752,10 @@ var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1500
1752
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1501
1753
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1502
1754
  var FIELDS = {
1503
- prompt: ["text", "files"],
1755
+ prompt: ["text", "files", "reply_to"],
1504
1756
  "permission.answer": ["for", "decision"],
1505
1757
  cancel: [],
1506
- start: ["text", "cwd", "files", "permission", "model"],
1758
+ start: ["text", "cwd", "files", "permission", "model", "reply_to"],
1507
1759
  "key.add": ["key", "name"],
1508
1760
  pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
1509
1761
  };
@@ -1511,9 +1763,454 @@ var POOL_FIELDS = FIELDS.pool;
1511
1763
  var enc = new TextEncoder();
1512
1764
  var dec = new TextDecoder();
1513
1765
 
1766
+ // ../core/src/adapters/claude-code-control.ts
1767
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1768
+ var LIVE_WRITE_MS = 9e4;
1769
+ var cmpVersion = (a, b) => {
1770
+ const pa = a.split(".").map(Number);
1771
+ const pb = b.split(".").map(Number);
1772
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++)
1773
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
1774
+ return 0;
1775
+ };
1776
+ function findClaude(env = process.env, home = os2.homedir()) {
1777
+ if (env.SESSIONPIPE_CLAUDE && isExe(env.SESSIONPIPE_CLAUDE)) return env.SESSIONPIPE_CLAUDE;
1778
+ const names = process.platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
1779
+ for (const d of String(env.PATH || "").split(path7.delimiter)) {
1780
+ if (!d || !path7.isAbsolute(d)) continue;
1781
+ for (const n of names) if (isExe(path7.join(d, n))) return path7.join(d, n);
1782
+ }
1783
+ for (const p of [
1784
+ path7.join(home, ".claude", "local", "claude"),
1785
+ path7.join(home, ".local", "bin", "claude"),
1786
+ "/opt/homebrew/bin/claude",
1787
+ "/usr/local/bin/claude"
1788
+ ])
1789
+ if (isExe(p)) return p;
1790
+ const app = path7.join(home, "Library", "Application Support", "Claude", "claude-code");
1791
+ let versions = [];
1792
+ try {
1793
+ versions = readdirSync4(app).filter((n) => /^\d+(\.\d+)*$/.test(n)).sort(cmpVersion).reverse();
1794
+ } catch {
1795
+ }
1796
+ for (const v of versions) {
1797
+ const p = path7.join(app, v, "claude.app", "Contents", "MacOS", "claude");
1798
+ if (isExe(p)) return p;
1799
+ }
1800
+ return null;
1801
+ }
1802
+ function optionBlock(help, flag) {
1803
+ const lines = help.split("\n");
1804
+ const re = new RegExp(`^\\s{0,4}(?:-\\w, )?${flag.replace(/-/g, "\\-")}(?![\\w-])`);
1805
+ const start = lines.findIndex((l) => re.test(l));
1806
+ if (start < 0) return null;
1807
+ const out = [lines[start]];
1808
+ for (let i = start + 1; i < lines.length && lines[i]?.trim() && !/^\s{0,4}-/.test(lines[i]); i++)
1809
+ out.push(lines[i]);
1810
+ return out.join(" ");
1811
+ }
1812
+ function parseCaps(help) {
1813
+ const pm = optionBlock(help, "--permission-mode");
1814
+ const pp = optionBlock(help, "--permission-prompts");
1815
+ return {
1816
+ hasMode: !!pm,
1817
+ modes: pm ? [...pm.matchAll(/"([A-Za-z]+)"/g)].map((m) => m[1]) : [],
1818
+ promptsNone: !!pp && /"none"/.test(pp),
1819
+ fork: !!optionBlock(help, "--fork-session"),
1820
+ stream: !!optionBlock(help, "--include-partial-messages")
1821
+ };
1822
+ }
1823
+ var LEVEL_MODE = {
1824
+ read: "plan",
1825
+ edits: "acceptEdits",
1826
+ auto: "auto"
1827
+ };
1828
+ function modeFlags(level, caps) {
1829
+ if (level === "inherit") return { args: [] };
1830
+ if (caps.unread)
1831
+ return { error: `this machine couldn't read its Claude Code's options (${caps.unread}); try again in a minute` };
1832
+ if (level === "auto" && !caps.modes.includes("auto"))
1833
+ return { error: "this machine is in auto mode, but its Claude Code has no auto permission mode" };
1834
+ if (level !== "safe") {
1835
+ const m = LEVEL_MODE[level];
1836
+ if (!caps.modes.includes(m)) return { error: `this machine's Claude Code has no ${m} permission mode` };
1837
+ return { args: ["--permission-mode", m] };
1838
+ }
1839
+ if (caps.modes.includes("dontAsk")) return { args: ["--permission-mode", "dontAsk"] };
1840
+ const args = [];
1841
+ if (caps.hasMode)
1842
+ args.push(
1843
+ "--permission-mode",
1844
+ caps.modes.includes("manual") && !caps.modes.includes("default") ? "manual" : "default"
1845
+ );
1846
+ if (caps.promptsNone) args.push("--permission-prompts", "none");
1847
+ return { args };
1848
+ }
1849
+ var ceilingLevel = (mode) => mode === "inherit" ? "safe" : mode;
1850
+ function offeredLevels(mode, caps) {
1851
+ if (caps.unread) return [];
1852
+ return START_PERMISSIONS.filter(
1853
+ (l) => levelRank(l) <= levelRank(ceilingLevel(mode)) && (l === "safe" || caps.modes.includes(LEVEL_MODE[l]))
1854
+ );
1855
+ }
1856
+ var defaultLevel = (mode) => mode;
1857
+ function outputArgs(caps) {
1858
+ return caps.stream ? ["--output-format", "stream-json", "--verbose", "--include-partial-messages"] : ["--output-format", "json"];
1859
+ }
1860
+ function claudeArgs(o) {
1861
+ const mf = modeFlags(o.level, o.caps);
1862
+ if ("error" in mf) return mf;
1863
+ const where = o.resume ? ["--resume", o.resume, ...o.fork ? ["--fork-session"] : []] : ["--session-id", o.session];
1864
+ return {
1865
+ args: [
1866
+ "-p",
1867
+ ...outputArgs(o.caps),
1868
+ ...mf.args,
1869
+ ...o.model ? ["--model", o.model] : [],
1870
+ ...o.allowedTools?.length ? [`--allowedTools=${o.allowedTools.join(",")}`] : [],
1871
+ ...where,
1872
+ o.prompt.startsWith("-") ? ` ${o.prompt}` : o.prompt
1873
+ ]
1874
+ };
1875
+ }
1876
+ function claudeEnv(configDir, env = process.env) {
1877
+ const e = withoutLineage(env);
1878
+ for (const k of [
1879
+ "CLAUDECODE",
1880
+ "CLAUDE_CODE_SESSION_ID",
1881
+ "CLAUDE_CODE_HOST_SESSION_ID",
1882
+ "CLAUDE_CODE_ENTRYPOINT",
1883
+ "CLAUDE_CODE_SESSION_ATTENDED",
1884
+ "CLAUDE_CODE_SSE_PORT"
1885
+ ])
1886
+ delete e[k];
1887
+ e.SESSIONPIPE_CONTROL_JOB = "1";
1888
+ if (configDir !== void 0) {
1889
+ if (path7.resolve(configDir) === path7.resolve(os2.homedir(), ".claude")) delete e.CLAUDE_CONFIG_DIR;
1890
+ else e.CLAUDE_CONFIG_DIR = configDir;
1891
+ }
1892
+ return e;
1893
+ }
1894
+ function parseAuthStatus(stdout) {
1895
+ const text = String(stdout || "").trim();
1896
+ const start = text.indexOf("{");
1897
+ if (start < 0) return { loggedIn: null };
1898
+ try {
1899
+ const j = JSON.parse(text.slice(start));
1900
+ if (typeof j.loggedIn !== "boolean") return { loggedIn: null };
1901
+ return {
1902
+ loggedIn: j.loggedIn,
1903
+ ...typeof j.authMethod === "string" ? { method: j.authMethod } : {},
1904
+ ...typeof j.subscriptionType === "string" ? { subscription: j.subscriptionType } : {}
1905
+ };
1906
+ } catch {
1907
+ return { loggedIn: null };
1908
+ }
1909
+ }
1910
+ function claudeLogin2(bin, configDir, env = process.env) {
1911
+ const c = probe(bin, ["auth", "status", "--json"]);
1912
+ return new Promise((resolve) => {
1913
+ execFile(
1914
+ c.file,
1915
+ c.args,
1916
+ { encoding: "utf8", timeout: 2e4, env: claudeEnv(configDir, env), maxBuffer: 256 * 1024, shell: c.shell },
1917
+ // `auth status` may exit non-zero when signed out; its JSON still says so.
1918
+ (_err, stdout) => resolve(parseAuthStatus(stdout))
1919
+ );
1920
+ });
1921
+ }
1922
+ function loginCommand(configDir) {
1923
+ const home = os2.homedir();
1924
+ const isDefault = !configDir || path7.resolve(configDir) === path7.resolve(home, ".claude");
1925
+ return isDefault ? "`claude auth login`" : `\`CLAUDE_CONFIG_DIR=${configDir.replace(home, "~")} claude auth login\``;
1926
+ }
1927
+ function loginHint(configDir, platform = process.platform) {
1928
+ const shown = configDir ? configDir.replace(os2.homedir(), "~") : "~/.claude";
1929
+ const login = loginCommand(configDir);
1930
+ if (platform === "darwin")
1931
+ 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.`;
1932
+ return `Claude Code (${shown}) isn't signed in on this machine: run ${login} there.`;
1933
+ }
1934
+ function detectCaps(bin, timeoutMs = 3e4) {
1935
+ const c = probe(bin, ["--help"]);
1936
+ try {
1937
+ return parseCaps(
1938
+ execFileSync3(c.file, c.args, {
1939
+ encoding: "utf8",
1940
+ timeout: timeoutMs,
1941
+ env: claudeEnv(),
1942
+ stdio: ["ignore", "pipe", "pipe"],
1943
+ maxBuffer: 4 * 1024 * 1024,
1944
+ shell: c.shell
1945
+ })
1946
+ );
1947
+ } catch (e) {
1948
+ const err = e;
1949
+ const caps = parseCaps(String(err?.stdout || ""));
1950
+ if (caps.hasMode) return caps;
1951
+ const why = err?.code === "ETIMEDOUT" ? `timed out after ${timeoutMs / 1e3} s` : "failed";
1952
+ return { ...caps, unread: `\`${path7.basename(bin)} --help\` ${why}` };
1953
+ }
1954
+ }
1955
+ function transcriptIn(session, dir) {
1956
+ let projects = [];
1957
+ try {
1958
+ projects = readdirSync4(path7.join(dir, "projects"), { withFileTypes: true });
1959
+ } catch {
1960
+ return null;
1961
+ }
1962
+ for (const p of projects) {
1963
+ if (!p.isDirectory()) continue;
1964
+ const file = path7.join(dir, "projects", p.name, `${session}.jsonl`);
1965
+ try {
1966
+ if (statSync5(file).isFile()) return { file, configDir: dir, project: p.name };
1967
+ } catch {
1968
+ }
1969
+ }
1970
+ return null;
1971
+ }
1972
+ function findTranscript(session, dirs) {
1973
+ if (!UUID_RE.test(session)) return null;
1974
+ for (const dir of dirs) {
1975
+ const t = transcriptIn(session, dir);
1976
+ if (t) return t;
1977
+ }
1978
+ return null;
1979
+ }
1980
+ function findTranscripts(session, dirs) {
1981
+ if (!UUID_RE.test(session)) return [];
1982
+ const out = [];
1983
+ for (const dir of dirs) {
1984
+ const t = transcriptIn(session, dir);
1985
+ if (t) out.push(t);
1986
+ }
1987
+ return out;
1988
+ }
1989
+ var homeName = (t) => path7.basename(t.configDir);
1990
+ function resolveHome(session, dirs) {
1991
+ const all = findTranscripts(session, dirs);
1992
+ if (!all.length) return null;
1993
+ if (all.length === 1) return { transcript: all[0] };
1994
+ const live = all.filter((t) => liveRecords(session, t.configDir).length > 0);
1995
+ if (live.length === 1) return { transcript: live[0] };
1996
+ return {
1997
+ ambiguous: [...new Set(all.map(homeName))].sort(),
1998
+ live: [...new Set(live.map(homeName))].sort()
1999
+ };
2000
+ }
2001
+ var projectName = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
2002
+ function sessionFolder(t, cap2 = 8 * 1024 * 1024) {
2003
+ let first = null;
2004
+ let count = 0;
2005
+ let fd;
2006
+ const look = (line) => {
2007
+ if (line.indexOf('"cwd"') < 0) return null;
2008
+ let j;
2009
+ try {
2010
+ j = JSON.parse(line.toString("utf8"));
2011
+ } catch {
2012
+ return null;
2013
+ }
2014
+ if (!j || typeof j.cwd !== "string" || !path7.isAbsolute(j.cwd)) return null;
2015
+ if (projectName(j.cwd) === t.project) return j.cwd;
2016
+ first ??= j.cwd;
2017
+ count++;
2018
+ return null;
2019
+ };
2020
+ try {
2021
+ fd = openSync2(t.file, "r");
2022
+ const buf = Buffer.alloc(256 * 1024);
2023
+ let pos = 0;
2024
+ let rest = Buffer.alloc(0);
2025
+ let skipping = false;
2026
+ while (pos < cap2 && count < 50) {
2027
+ const n = readSync2(fd, buf, 0, buf.length, pos);
2028
+ if (!n) {
2029
+ if (!skipping && rest.length) {
2030
+ const hit = look(rest);
2031
+ if (hit) return hit;
2032
+ }
2033
+ break;
2034
+ }
2035
+ pos += n;
2036
+ let chunk = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
2037
+ let nl = chunk.indexOf(10);
2038
+ while (nl >= 0) {
2039
+ const line = chunk.subarray(0, nl);
2040
+ chunk = chunk.subarray(nl + 1);
2041
+ if (skipping) skipping = false;
2042
+ else {
2043
+ const hit = look(line);
2044
+ if (hit) return hit;
2045
+ }
2046
+ nl = chunk.indexOf(10);
2047
+ }
2048
+ if (chunk.length > 2 * 1024 * 1024) {
2049
+ skipping = true;
2050
+ rest = Buffer.alloc(0);
2051
+ } else rest = skipping ? Buffer.alloc(0) : Buffer.from(chunk);
2052
+ }
2053
+ } catch {
2054
+ } finally {
2055
+ if (fd !== void 0)
2056
+ try {
2057
+ closeSync2(fd);
2058
+ } catch {
2059
+ }
2060
+ }
2061
+ return first;
2062
+ }
2063
+ function pidAlive(pid) {
2064
+ if (!Number.isSafeInteger(pid) || pid <= 1) return false;
2065
+ try {
2066
+ process.kill(pid, 0);
2067
+ return true;
2068
+ } catch (e) {
2069
+ return e.code === "EPERM";
2070
+ }
2071
+ }
2072
+ var registryRow = (file, configDir, j) => {
2073
+ const pid = Number(j.pid ?? path7.basename(file).slice(0, -5));
2074
+ const str = (v) => typeof v === "string" && v ? v : null;
2075
+ let keyFile = null;
2076
+ try {
2077
+ const want = new RegExp(`^${pid}\\.[0-9a-f]{64}\\.key$`);
2078
+ const hit = readdirSync4(path7.dirname(file)).find((n) => want.test(n));
2079
+ if (hit) keyFile = path7.join(path7.dirname(file), hit);
2080
+ } catch {
2081
+ }
2082
+ return {
2083
+ configDir,
2084
+ file,
2085
+ pid,
2086
+ socket: str(j.messagingSocketPath),
2087
+ peerProtocol: typeof j.peerProtocol === "number" ? j.peerProtocol : null,
2088
+ procStart: str(j.procStart),
2089
+ status: str(j.status),
2090
+ kind: str(j.kind),
2091
+ entrypoint: str(j.entrypoint),
2092
+ keyFile
2093
+ };
2094
+ };
2095
+ function liveRecords(session, dirs) {
2096
+ const out = [];
2097
+ for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
2098
+ let names = [];
2099
+ try {
2100
+ names = readdirSync4(path7.join(dir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
2101
+ } catch {
2102
+ continue;
2103
+ }
2104
+ for (const n of names) {
2105
+ const file = path7.join(dir, "sessions", n);
2106
+ let j;
2107
+ try {
2108
+ j = JSON.parse(readFileSync5(file, "utf8"));
2109
+ } catch {
2110
+ continue;
2111
+ }
2112
+ if (String(j.sessionId) !== session) continue;
2113
+ const row = registryRow(file, dir, j);
2114
+ if (pidAlive(row.pid)) out.push(row);
2115
+ }
2116
+ }
2117
+ return out;
2118
+ }
2119
+ function liveRecord(session, dirs) {
2120
+ const all = liveRecords(session, dirs);
2121
+ return all.length === 1 ? all[0] : null;
2122
+ }
2123
+ function liveProcess(session, configDir) {
2124
+ return liveRecords(session, configDir).length > 0;
2125
+ }
2126
+ var MODE_RANK = {
2127
+ plan: 0,
2128
+ default: 1,
2129
+ manual: 1,
2130
+ dontAsk: 1,
2131
+ acceptEdits: 2,
2132
+ auto: 3,
2133
+ bypassPermissions: 4
2134
+ };
2135
+ var CLAUDE_MODES = Object.keys(MODE_RANK);
2136
+ var modeRank = (m) => m && m in MODE_RANK ? MODE_RANK[m] : -1;
2137
+ function hookMode(payload) {
2138
+ if (!payload || typeof payload !== "object") return null;
2139
+ const p = payload;
2140
+ const v = typeof p.permission_mode === "string" ? p.permission_mode : p.permissionMode;
2141
+ return typeof v === "string" && CLAUDE_MODES.includes(v) ? v : null;
2142
+ }
2143
+ function peerClass(mode) {
2144
+ if (mode === "bypassPermissions") return "bypass";
2145
+ if (mode === "default" || mode === "manual" || mode === "dontAsk" || mode === "acceptEdits") return "prompting";
2146
+ return null;
2147
+ }
2148
+ var NO_HOME_SETTINGS = { defaultMode: null, disableAutoMode: false, disableBypass: false };
2149
+ function homeSettings(configDir, home = os2.homedir()) {
2150
+ const file = path7.join(configDir ?? path7.join(home, ".claude"), "settings.json");
2151
+ let j;
2152
+ try {
2153
+ j = JSON.parse(readFileSync5(file, "utf8"));
2154
+ } catch {
2155
+ return { ...NO_HOME_SETTINGS };
2156
+ }
2157
+ if (!j || typeof j !== "object" || Array.isArray(j)) return { ...NO_HOME_SETTINGS };
2158
+ const p = j.permissions && typeof j.permissions === "object" ? j.permissions : {};
2159
+ const off = (k) => j[k] === "disable" || p[k] === "disable";
2160
+ return {
2161
+ defaultMode: typeof p.defaultMode === "string" && CLAUDE_MODES.includes(p.defaultMode) ? p.defaultMode : null,
2162
+ disableAutoMode: off("disableAutoMode"),
2163
+ disableBypass: off("disableBypassPermissionsMode")
2164
+ };
2165
+ }
2166
+ function relaunchMode(recorded, home) {
2167
+ if (recorded === "plan") return recorded;
2168
+ if ((recorded === "dontAsk" || recorded === "acceptEdits") && modeRank(home.defaultMode) >= modeRank(recorded))
2169
+ return recorded;
2170
+ return null;
2171
+ }
2172
+ function recentlyWritten(file, ourLastEnd, now = Date.now()) {
2173
+ let m;
2174
+ try {
2175
+ m = statSync5(file).mtimeMs;
2176
+ } catch {
2177
+ return false;
2178
+ }
2179
+ if (now - m > LIVE_WRITE_MS) return false;
2180
+ return !(ourLastEnd && m <= ourLastEnd + 2e3);
2181
+ }
2182
+ function runClaude(bin, args, o) {
2183
+ return runHeadless(bin, args, { ...o, keep: o.keep ?? ((line) => line.includes('"type":"result"')) });
2184
+ }
2185
+ function parseResult(stdout) {
2186
+ const s = String(stdout || "").trim();
2187
+ if (!s) return null;
2188
+ const pick = (j) => {
2189
+ if (Array.isArray(j)) {
2190
+ for (let i = j.length - 1; i >= 0; i--) if (j[i]?.type === "result") return j[i];
2191
+ return null;
2192
+ }
2193
+ return j && typeof j === "object" ? j : null;
2194
+ };
2195
+ try {
2196
+ return pick(JSON.parse(s));
2197
+ } catch {
2198
+ }
2199
+ const lines = s.split("\n");
2200
+ for (let i = lines.length - 1; i >= 0; i--) {
2201
+ try {
2202
+ const j = pick(JSON.parse(lines[i]));
2203
+ if (j && (j.type === "result" || "result" in j)) return j;
2204
+ } catch {
2205
+ }
2206
+ }
2207
+ return null;
2208
+ }
2209
+ var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
2210
+
1514
2211
  // ../core/src/adapters/codex.ts
1515
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1516
- import path7 from "node:path";
2212
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
2213
+ import path8 from "node:path";
1517
2214
 
1518
2215
  // ../core/src/adapters/codex-config.ts
1519
2216
  var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
@@ -1659,35 +2356,36 @@ var CODEX_EVENTS = [
1659
2356
  ];
1660
2357
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1661
2358
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1662
- var codexHome = (env) => env.CODEX_HOME || path7.join(homeDir(), ".codex");
1663
- var hooksFile2 = (env) => path7.join(codexHome(env), "hooks.json");
1664
- var configFile2 = (env) => path7.join(codexHome(env), "config.toml");
1665
- var sessionsDir = (env) => path7.join(codexHome(env), "sessions");
2359
+ var codexHome = (env) => env.CODEX_HOME || path8.join(homeDir(), ".codex");
2360
+ var hooksFile2 = (env) => path8.join(codexHome(env), "hooks.json");
2361
+ var configFile2 = (env) => path8.join(codexHome(env), "config.toml");
2362
+ var sessionsDir = (env) => path8.join(codexHome(env), "sessions");
1666
2363
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1667
2364
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
2365
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
1668
2366
  function installNotify(cmd, env) {
1669
2367
  const file = configFile2(env);
1670
2368
  let text = "";
2369
+ let existed = true;
1671
2370
  try {
1672
- text = readFileSync5(file, "utf8");
1673
- } catch {
2371
+ text = readFileSync6(file, "utf8");
2372
+ } catch (e) {
2373
+ existed = e.code !== "ENOENT";
1674
2374
  }
1675
2375
  const argv = cmd([NAME3, "notify"]).argv;
1676
2376
  const line = `notify = ${JSON.stringify(argv)}`;
1677
2377
  const n = readNotify(text);
1678
- const write = (next) => {
1679
- mkdirSync3(path7.dirname(file), { recursive: true });
1680
- writeFileSync4(file, next);
2378
+ const write = (next, note) => {
2379
+ const bad = writeBack(file, next, existed);
2380
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
1681
2381
  };
1682
2382
  if (n && !n.other) {
1683
2383
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1684
2384
  return { file, changed: false, note: "notify current" };
1685
- write(withNotify(text, line));
1686
- return {
1687
- file,
1688
- changed: true,
1689
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1690
- };
2385
+ return write(
2386
+ withNotify(text, line),
2387
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
2388
+ );
1691
2389
  }
1692
2390
  if (n?.other) {
1693
2391
  const prev = previousNotify(n.argv);
@@ -1696,40 +2394,38 @@ function installNotify(cmd, env) {
1696
2394
  if (JSON.stringify(prev) === JSON.stringify(argv))
1697
2395
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1698
2396
  }
1699
- if (!prev || OURS2.test(JSON.stringify(prev))) {
1700
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1701
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1702
- }
2397
+ if (!prev || OURS2.test(JSON.stringify(prev)))
2398
+ return write(
2399
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
2400
+ "chained after Codex Computer Use (--previous-notify)"
2401
+ );
1703
2402
  }
1704
2403
  return {
1705
2404
  file,
1706
2405
  changed: false,
1707
2406
  skipped: true,
1708
- note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. Codex will report nothing until you approve the hooks: start \`codex\` and accept the hook review`
2407
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
1709
2408
  };
1710
2409
  }
1711
- write(withNotify(text, line));
1712
- return { file, changed: true, note: "notify set at the top level" };
2410
+ return write(withNotify(text, line), "notify set at the top level");
1713
2411
  }
1714
2412
  function uninstallNotify(env) {
1715
2413
  const file = configFile2(env);
1716
2414
  let text;
1717
2415
  try {
1718
- text = readFileSync5(file, "utf8");
2416
+ text = readFileSync6(file, "utf8");
1719
2417
  } catch {
1720
2418
  return { file, changed: false };
1721
2419
  }
1722
2420
  const n = readNotify(text);
1723
2421
  if (!n) return { file, changed: false };
1724
- if (!n.other) {
1725
- writeFileSync4(file, withoutNotify(text));
1726
- return { file, changed: true };
1727
- }
2422
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
1728
2423
  const prev = previousNotify(n.argv);
1729
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1730
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1731
- return { file, changed: true };
1732
- }
2424
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
2425
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
2426
+ file,
2427
+ changed: true
2428
+ };
1733
2429
  return { file, changed: false };
1734
2430
  }
1735
2431
  function findRollout(threadId, env = process.env) {
@@ -1738,12 +2434,12 @@ function findRollout(threadId, env = process.env) {
1738
2434
  const walk = (dir, depth) => {
1739
2435
  let names = [];
1740
2436
  try {
1741
- names = readdirSync4(dir);
2437
+ names = readdirSync5(dir);
1742
2438
  } catch {
1743
2439
  return;
1744
2440
  }
1745
2441
  for (const n of names.sort().reverse()) {
1746
- const p = path7.join(dir, n);
2442
+ const p = path8.join(dir, n);
1747
2443
  if (n.endsWith(`-${threadId}.jsonl`)) {
1748
2444
  best = p;
1749
2445
  return;
@@ -1761,8 +2457,12 @@ function rolloutMeta(file) {
1761
2457
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
1762
2458
  }
1763
2459
  function ofAnotherThread(file, id) {
2460
+ const other = threadOf(file);
2461
+ return !!other && other !== id.toLowerCase();
2462
+ }
2463
+ function threadOf(file) {
1764
2464
  const m = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i.exec(file);
1765
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
2465
+ return m?.[1] ? m[1].toLowerCase() : null;
1766
2466
  }
1767
2467
  var codex = {
1768
2468
  name: NAME3,
@@ -1787,7 +2487,7 @@ var codex = {
1787
2487
  ];
1788
2488
  let text = "";
1789
2489
  try {
1790
- text = readFileSync5(configFile2(env), "utf8");
2490
+ text = readFileSync6(configFile2(env), "utf8");
1791
2491
  } catch {
1792
2492
  }
1793
2493
  const n = readNotify(text);
@@ -1841,6 +2541,8 @@ var codex = {
1841
2541
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1842
2542
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1843
2543
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2544
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
2545
+ const agent_id = typeof s.agent_id === "string" && s.agent_id ? s.agent_id : typeof s.subagent_id === "string" && s.subagent_id ? s.subagent_id : thread && thread !== String(id).toLowerCase() ? thread : void 0;
1844
2546
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1845
2547
  const events = [];
1846
2548
  switch (event) {
@@ -1860,17 +2562,30 @@ var codex = {
1860
2562
  );
1861
2563
  break;
1862
2564
  case "PreToolUse":
1863
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2565
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
1864
2566
  break;
1865
2567
  case "PostToolUse":
1866
2568
  if (tool)
1867
2569
  events.push(
1868
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
2570
+ ev("tool.ended", {
2571
+ tool,
2572
+ call_id,
2573
+ turn_id,
2574
+ agent_id,
2575
+ ok: true,
2576
+ input: s.tool_input,
2577
+ output: s.tool_response
2578
+ })
1869
2579
  );
1870
2580
  break;
1871
2581
  case "PermissionRequest":
1872
2582
  events.push(
1873
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
2583
+ ev("attention.needed", {
2584
+ attention_id: call_id ?? `perm-${Date.now()}`,
2585
+ kind: "permission",
2586
+ tool,
2587
+ agent_id
2588
+ })
1874
2589
  );
1875
2590
  break;
1876
2591
  case "Stop":
@@ -1914,7 +2629,7 @@ var codex = {
1914
2629
  readTranscript(file, fromLine) {
1915
2630
  let text = "";
1916
2631
  try {
1917
- text = readFileSync5(file, "utf8");
2632
+ text = readFileSync6(file, "utf8");
1918
2633
  } catch {
1919
2634
  return { turns: [], line: fromLine };
1920
2635
  }
@@ -1928,7 +2643,7 @@ var codex = {
1928
2643
  backfill(sinceMs, env = process.env) {
1929
2644
  const rows2 = [];
1930
2645
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1931
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path7.basename(f));
2646
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path8.basename(f));
1932
2647
  if (!m?.[1]) continue;
1933
2648
  if (codexSubagent(rolloutMeta(f))) continue;
1934
2649
  const facts = codexFacts(f);
@@ -2015,10 +2730,10 @@ function codexFacts(file) {
2015
2730
  // ../core/src/adapters/devin.ts
2016
2731
  import { createHash as createHash2 } from "node:crypto";
2017
2732
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
2018
- import path8 from "node:path";
2733
+ import path9 from "node:path";
2019
2734
 
2020
2735
  // ../core/src/readers/sqlite.ts
2021
- import { execFileSync as execFileSync3 } from "node:child_process";
2736
+ import { execFileSync as execFileSync4 } from "node:child_process";
2022
2737
  import { existsSync as existsSync5 } from "node:fs";
2023
2738
  import { createRequire } from "node:module";
2024
2739
  function quiet(fn) {
@@ -2098,7 +2813,7 @@ db.close();
2098
2813
  process.stdout.write(JSON.stringify(out));`;
2099
2814
  function viaChild(file, sql, params, o) {
2100
2815
  try {
2101
- const out = execFileSync3(
2816
+ const out = execFileSync4(
2102
2817
  process.execPath,
2103
2818
  ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
2104
2819
  {
@@ -2142,7 +2857,7 @@ function viaCli(file, sql, params, o) {
2142
2857
  const text = inlineParams(sql, params);
2143
2858
  if (!text) return null;
2144
2859
  try {
2145
- const out = execFileSync3("sqlite3", ["-readonly", "-json", file, text], {
2860
+ const out = execFileSync4("sqlite3", ["-readonly", "-json", file, text], {
2146
2861
  encoding: "utf8",
2147
2862
  timeout: o.timeoutMs + 3e3,
2148
2863
  stdio: ["ignore", "pipe", "ignore"],
@@ -2199,11 +2914,11 @@ function foreignEvent(file) {
2199
2914
  return Object.keys(hooks).find((k) => !ok.includes(k));
2200
2915
  }
2201
2916
  var homeOf = (env) => env.HOME || homeDir();
2202
- var appData = (env) => env.APPDATA || path8.join(homeOf(env), "AppData", "Roaming");
2203
- 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");
2204
- var configFile3 = (env) => path8.join(devinConfigDir(env), "config.json");
2205
- 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");
2206
- var sessionsDb = (env) => path8.join(devinDataDir(env), "sessions.db");
2917
+ var appData = (env) => env.APPDATA || path9.join(homeOf(env), "AppData", "Roaming");
2918
+ 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");
2919
+ var configFile3 = (env) => path9.join(devinConfigDir(env), "config.json");
2920
+ 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");
2921
+ var sessionsDb = (env) => path9.join(devinDataDir(env), "sessions.db");
2207
2922
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
2208
2923
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2209
2924
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2360,7 +3075,7 @@ var CHAIN_LIMIT = 4e3;
2360
3075
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2361
3076
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2362
3077
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2363
- UNION ALL
3078
+ UNION
2364
3079
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2365
3080
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2366
3081
  )
@@ -2375,7 +3090,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2375
3090
  function sessionFacts(row) {
2376
3091
  const out = {};
2377
3092
  if (typeof row.model === "string" && row.model) out.model = row.model;
2378
- if (typeof row.working_directory === "string" && path8.isAbsolute(row.working_directory))
3093
+ if (typeof row.working_directory === "string" && path9.isAbsolute(row.working_directory))
2379
3094
  out.cwd = row.working_directory;
2380
3095
  const started = Number(row.created_at);
2381
3096
  const last = Number(row.last_activity_at);
@@ -2420,8 +3135,8 @@ function readDevinChain(db, session, fromLine) {
2420
3135
  }
2421
3136
  function devinVersion(env = process.env) {
2422
3137
  try {
2423
- const target = readlinkSync(path8.join(devinDataDir(env), "_versions", "current"));
2424
- const name = path8.basename(target);
3138
+ const target = readlinkSync(path9.join(devinDataDir(env), "_versions", "current"));
3139
+ const name = path9.basename(target);
2425
3140
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2426
3141
  } catch {
2427
3142
  return void 0;
@@ -2434,9 +3149,9 @@ function devinAccount(env = process.env) {
2434
3149
  }
2435
3150
 
2436
3151
  // ../core/src/adapters/gemini-cli.ts
2437
- import { execFileSync as execFileSync4 } from "node:child_process";
2438
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2439
- import path9 from "node:path";
3152
+ import { execFileSync as execFileSync5 } from "node:child_process";
3153
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
3154
+ import path10 from "node:path";
2440
3155
  var NAME5 = "gemini-cli";
2441
3156
  var GEMINI_EVENTS = [
2442
3157
  "SessionStart",
@@ -2449,16 +3164,16 @@ var GEMINI_EVENTS = [
2449
3164
  "SessionEnd"
2450
3165
  ];
2451
3166
  var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2452
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path9.join(homeDir(), ".gemini");
2453
- var settings = (env) => path9.join(geminiDir(env), "settings.json");
3167
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path10.join(homeDir(), ".gemini");
3168
+ var settings = (env) => path10.join(geminiDir(env), "settings.json");
2454
3169
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2455
3170
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2456
3171
  function geminiVersion(platform = process.platform, env = process.env) {
2457
3172
  const win = platform === "win32";
2458
- const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path9.dirname(process.execPath)], env }) : "gemini";
3173
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path10.dirname(process.execPath)], env }) : "gemini";
2459
3174
  if (!bin) throw new Error("no gemini on PATH");
2460
3175
  const c = probe(bin, ["--version"]);
2461
- return execFileSync4(c.file, c.args, {
3176
+ return execFileSync5(c.file, c.args, {
2462
3177
  encoding: "utf8",
2463
3178
  timeout: 4e3,
2464
3179
  stdio: ["ignore", "pipe", "ignore"],
@@ -2491,7 +3206,7 @@ var geminiCli = {
2491
3206
  name: NAME5,
2492
3207
  events: GEMINI_EVENTS,
2493
3208
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2494
- detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path9.join(geminiDir(env), "tmp")),
3209
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path10.join(geminiDir(env), "tmp")),
2495
3210
  configFiles: (env = process.env) => [settings(env)],
2496
3211
  install: (cmd, env = process.env, opts = {}) => {
2497
3212
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2589,7 +3304,7 @@ var geminiCli = {
2589
3304
  readTranscript(file, fromLine) {
2590
3305
  let text = "";
2591
3306
  try {
2592
- text = readFileSync6(file, "utf8");
3307
+ text = readFileSync7(file, "utf8");
2593
3308
  } catch {
2594
3309
  return { turns: [], line: fromLine };
2595
3310
  }
@@ -2629,12 +3344,12 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2629
3344
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2630
3345
 
2631
3346
  // ../core/src/config.ts
2632
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
2633
- import path10 from "node:path";
3347
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
3348
+ import path11 from "node:path";
2634
3349
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2635
3350
  function readConfig(file = configFile()) {
2636
3351
  try {
2637
- const j = JSON.parse(readFileSync7(file, "utf8"));
3352
+ const j = JSON.parse(readFileSync8(file, "utf8"));
2638
3353
  return {
2639
3354
  ...DEFAULT_CONFIG,
2640
3355
  ...j,
@@ -2645,9 +3360,36 @@ function readConfig(file = configFile()) {
2645
3360
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
2646
3361
  }
2647
3362
  }
3363
+ function configProblem(file) {
3364
+ let text;
3365
+ try {
3366
+ text = readFileSync8(file, "utf8");
3367
+ } catch (e) {
3368
+ const code = e.code;
3369
+ return code === "ENOENT" ? void 0 : `could not be read (${code})`;
3370
+ }
3371
+ if (!text.trim()) return void 0;
3372
+ if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
3373
+ try {
3374
+ const j = JSON.parse(text);
3375
+ return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
3376
+ } catch (e) {
3377
+ const m = e.message;
3378
+ const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
3379
+ return `is not valid JSON${at ? ` at ${at}` : ""}`;
3380
+ }
3381
+ }
3382
+ function assertConfigWritable(file = configFile()) {
3383
+ const problem = configProblem(file);
3384
+ if (problem)
3385
+ throw new Error(
3386
+ `${tilde(file)} ${problem}, so it was not written over: that would lose its machine name and sinks. Fix it, or move it aside to start again, then run this again.`
3387
+ );
3388
+ }
2648
3389
  function writeConfig(c, file = configFile()) {
2649
- mkdirSync4(path10.dirname(file), { recursive: true, mode: 448 });
2650
- writeFileSync5(file, `${JSON.stringify(c, null, 2)}
3390
+ assertConfigWritable(file);
3391
+ mkdirSync2(path11.dirname(file), { recursive: true, mode: 448 });
3392
+ writeFileSync2(file, `${JSON.stringify(c, null, 2)}
2651
3393
  `, { mode: 384 });
2652
3394
  if (process.platform !== "win32" && existsSync8(file)) {
2653
3395
  try {
@@ -2710,38 +3452,23 @@ function makeEvent(a, ctx) {
2710
3452
  };
2711
3453
  }
2712
3454
 
2713
- // ../core/src/launched-by.ts
2714
- var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2715
- var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2716
- function launchedBy(env) {
2717
- const v = env[LAUNCHED_BY_ENV]?.trim();
2718
- return v && LAUNCHED_BY.test(v) ? v : void 0;
2719
- }
2720
- function readHook(adapter, input) {
2721
- const r = adapter.fromHook(input);
2722
- if (r && !r.session.parent_id) {
2723
- const launcher = launchedBy(input.env);
2724
- if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2725
- }
2726
- return r;
2727
- }
2728
-
2729
3455
  // ../core/src/outbox.ts
3456
+ import { randomBytes as randomBytes2 } from "node:crypto";
2730
3457
  import {
2731
3458
  appendFileSync,
2732
- closeSync as closeSync2,
3459
+ closeSync as closeSync3,
2733
3460
  existsSync as existsSync9,
2734
- mkdirSync as mkdirSync5,
2735
- openSync as openSync2,
2736
- readdirSync as readdirSync5,
2737
- readFileSync as readFileSync8,
2738
- readSync as readSync2,
3461
+ mkdirSync as mkdirSync3,
3462
+ openSync as openSync3,
3463
+ readdirSync as readdirSync6,
3464
+ readFileSync as readFileSync9,
3465
+ readSync as readSync3,
2739
3466
  renameSync,
2740
- statSync as statSync5,
2741
- unlinkSync as unlinkSync3,
2742
- writeFileSync as writeFileSync6
3467
+ statSync as statSync6,
3468
+ unlinkSync as unlinkSync2,
3469
+ writeFileSync as writeFileSync3
2743
3470
  } from "node:fs";
2744
- import path11 from "node:path";
3471
+ import path12 from "node:path";
2745
3472
  var READ_CHUNK = 1024 * 1024;
2746
3473
  var READ_MAX = 8 * 1024 * 1024;
2747
3474
  var Outbox = class {
@@ -2750,13 +3477,13 @@ var Outbox = class {
2750
3477
  }
2751
3478
  root;
2752
3479
  dir(harness) {
2753
- return path11.join(this.root, "outbox", safeId(harness));
3480
+ return path12.join(this.root, "outbox", safeId(harness));
2754
3481
  }
2755
3482
  file(ref) {
2756
- return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
3483
+ return path12.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2757
3484
  }
2758
3485
  seqFile(ref) {
2759
- return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
3486
+ return path12.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2760
3487
  }
2761
3488
  /** The next seq for a session, persisted (survives a hibernating machine and a
2762
3489
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2764,19 +3491,19 @@ var Outbox = class {
2764
3491
  const f = this.seqFile(ref);
2765
3492
  let n = -1;
2766
3493
  try {
2767
- const t = readFileSync8(f, "utf8").trim();
3494
+ const t = readFileSync9(f, "utf8").trim();
2768
3495
  if (/^\d+$/.test(t)) n = Number(t);
2769
3496
  } catch {
2770
3497
  }
2771
3498
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
2772
- mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2773
- writeFileSync6(f, String(n + 1), { mode: 384 });
3499
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
3500
+ writeFileSync3(f, String(n + 1), { mode: 384 });
2774
3501
  return n;
2775
3502
  }
2776
3503
  lastSeqInFile(ref) {
2777
3504
  const f = this.file(ref);
2778
3505
  if (!existsSync9(f)) return -1;
2779
- const lines = readFileSync8(f, "utf8").trim().split("\n");
3506
+ const lines = readFileSync9(f, "utf8").trim().split("\n");
2780
3507
  for (let i = lines.length - 1; i >= 0; i--) {
2781
3508
  try {
2782
3509
  const j = JSON.parse(lines[i]);
@@ -2788,26 +3515,26 @@ var Outbox = class {
2788
3515
  }
2789
3516
  append(ref, events) {
2790
3517
  if (!events.length) return;
2791
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
3518
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
2792
3519
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
2793
3520
  `, { mode: 384 });
2794
3521
  }
2795
3522
  /** Every session file, newest first. */
2796
3523
  sessions() {
2797
3524
  const out = [];
2798
- const base = path11.join(this.root, "outbox");
3525
+ const base = path12.join(this.root, "outbox");
2799
3526
  if (!existsSync9(base)) return [];
2800
- for (const h of readdirSync5(base)) {
2801
- const d = path11.join(base, h);
3527
+ for (const h of readdirSync6(base)) {
3528
+ const d = path12.join(base, h);
2802
3529
  let names = [];
2803
3530
  try {
2804
- names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
3531
+ names = readdirSync6(d).filter((n) => n.endsWith(".jsonl"));
2805
3532
  } catch {
2806
3533
  continue;
2807
3534
  }
2808
3535
  for (const n of names) {
2809
3536
  try {
2810
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path11.join(d, n)).mtimeMs });
3537
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(path12.join(d, n)).mtimeMs });
2811
3538
  } catch {
2812
3539
  }
2813
3540
  }
@@ -2823,16 +3550,16 @@ var Outbox = class {
2823
3550
  read(ref, from, maxEvents = 500) {
2824
3551
  const f = this.file(ref);
2825
3552
  if (!existsSync9(f)) return { events: [], offset: from };
2826
- const size = statSync5(f).size;
3553
+ const size = statSync6(f).size;
2827
3554
  if (from >= size) return { events: [], offset: from };
2828
- const fd = openSync2(f, "r");
3555
+ const fd = openSync3(f, "r");
2829
3556
  try {
2830
3557
  const events = [];
2831
3558
  let offset = from;
2832
3559
  let chunk = READ_CHUNK;
2833
3560
  while (events.length < maxEvents && offset < size) {
2834
3561
  const buf = Buffer.alloc(Math.min(size - offset, chunk));
2835
- const n = readSync2(fd, buf, 0, buf.length, offset);
3562
+ const n = readSync3(fd, buf, 0, buf.length, offset);
2836
3563
  const text = buf.subarray(0, n).toString("utf8");
2837
3564
  let pos = 0;
2838
3565
  let used = 0;
@@ -2855,37 +3582,109 @@ var Outbox = class {
2855
3582
  }
2856
3583
  return { events, offset };
2857
3584
  } finally {
2858
- closeSync2(fd);
3585
+ closeSync3(fd);
2859
3586
  }
2860
3587
  }
2861
- /** Files older than `days` are pruned; the cursor files with them. */
3588
+ /** Files older than `days` are pruned, and each sink's cursor into them. The seq
3589
+ * counter stays: a session resumed after the window counts on from where it was,
3590
+ * so a receiver's (harness, session, seq) dedup doesn't take its new events for
3591
+ * ones it already has, and the sinks read the new file from its start.
3592
+ * It all happens under the drain's `flush` lock, since a drain holds the whole
3593
+ * cursor map and writes it back: a file and its cursor keys go together, so a
3594
+ * session resumed in between can't keep a key past its new file's end. While a
3595
+ * drain runs, nothing is pruned (0); the next prune does it. It never throws: it
3596
+ * is a cleanup, and a hook's drain comes after it. */
2862
3597
  prune(days, now = Date.now()) {
3598
+ let release = null;
3599
+ try {
3600
+ release = takeNamedLock(this.root, "flush");
3601
+ } catch {
3602
+ }
3603
+ if (!release) return 0;
2863
3604
  let n = 0;
2864
- const cutoff = now - days * 864e5;
2865
- for (const ref of this.sessions()) {
2866
- const f = this.file(ref);
2867
- try {
2868
- if (statSync5(f).mtimeMs < cutoff) {
2869
- unlinkSync3(f);
2870
- try {
2871
- unlinkSync3(this.seqFile(ref));
2872
- } catch {
3605
+ try {
3606
+ const cutoff = now - days * 864e5;
3607
+ const aged = this.sessions().filter((ref) => {
3608
+ try {
3609
+ return statSync6(this.file(ref)).mtimeMs < cutoff;
3610
+ } catch {
3611
+ return false;
3612
+ }
3613
+ });
3614
+ this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
3615
+ for (const ref of aged) {
3616
+ const f = this.file(ref);
3617
+ try {
3618
+ if (statSync6(f).mtimeMs < cutoff) {
3619
+ unlinkSync2(f);
3620
+ n++;
2873
3621
  }
2874
- n++;
3622
+ } catch {
2875
3623
  }
2876
- } catch {
2877
3624
  }
3625
+ } catch {
3626
+ } finally {
3627
+ release();
2878
3628
  }
2879
3629
  return n;
2880
3630
  }
3631
+ /** A sink's cursor into a session file about to be pruned (`gone`), or one already
3632
+ * missing (an earlier prune, an older release, by hand) is removed. The caller
3633
+ * holds the `flush` lock. */
3634
+ dropStaleCursors(gone) {
3635
+ let names = [];
3636
+ try {
3637
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3638
+ } catch {
3639
+ }
3640
+ const stale2 = (c) => Object.keys(c).filter((k) => {
3641
+ if (gone.has(k)) return true;
3642
+ const i = k.indexOf("/");
3643
+ return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
3644
+ });
3645
+ for (const n of names) {
3646
+ const s = new Cursors(this.root, n.slice(0, -5));
3647
+ const c = s.read();
3648
+ const keys = stale2(c);
3649
+ if (!keys.length) continue;
3650
+ for (const k of keys) delete c[k];
3651
+ s.write(c);
3652
+ }
3653
+ }
2881
3654
  delete(ref) {
3655
+ this.dropCursors(ref);
2882
3656
  for (const f of [this.file(ref), this.seqFile(ref)]) {
2883
3657
  try {
2884
- unlinkSync3(f);
3658
+ unlinkSync2(f);
2885
3659
  } catch {
2886
3660
  }
2887
3661
  }
2888
3662
  }
3663
+ /** The key every sink's cursor holds for a session is removed with the session's
3664
+ * file: the key alone pointed into the old file, and a session that went on (still
3665
+ * running, or resumed later) had its new file's first events unsent until it grew
3666
+ * past that offset. Every file under <state>/cursors is taken (a sink since taken
3667
+ * out of the config still has one; one that doesn't parse reads as empty and is
3668
+ * left), by the key a drain writes (the file's own name, `safeId`) and by the ref
3669
+ * as given. The caller holds the drain's `flush` lock: a drain holds the whole
3670
+ * cursor map and writes it back, so a key dropped under it would come back. */
3671
+ dropCursors(ref) {
3672
+ const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
3673
+ let names = [];
3674
+ try {
3675
+ names = readdirSync6(path12.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
3676
+ } catch {
3677
+ return;
3678
+ }
3679
+ for (const n of names) {
3680
+ const s = new Cursors(this.root, n.slice(0, -5));
3681
+ const c = s.read();
3682
+ const had = Object.keys(c).filter((k) => keys.has(k));
3683
+ if (!had.length) continue;
3684
+ for (const k of had) delete c[k];
3685
+ s.write(c);
3686
+ }
3687
+ }
2889
3688
  };
2890
3689
  var Cursors = class {
2891
3690
  constructor(root, sink) {
@@ -2895,20 +3694,23 @@ var Cursors = class {
2895
3694
  root;
2896
3695
  sink;
2897
3696
  file() {
2898
- return path11.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3697
+ return path12.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2899
3698
  }
3699
+ /** The map; empty when the file is missing, cut off mid-write, or holds anything
3700
+ * but an object (`null`, a list), so a drain never indexes into those. */
2900
3701
  read() {
2901
3702
  try {
2902
- return JSON.parse(readFileSync8(this.file(), "utf8"));
3703
+ const c = JSON.parse(readFileSync9(this.file(), "utf8"));
3704
+ return c && typeof c === "object" && !Array.isArray(c) ? c : {};
2903
3705
  } catch {
2904
3706
  return {};
2905
3707
  }
2906
3708
  }
2907
3709
  write(c) {
2908
3710
  const f = this.file();
2909
- mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
3711
+ mkdirSync3(path12.dirname(f), { recursive: true, mode: 448 });
2910
3712
  const tmp = `${f}.${process.pid}.tmp`;
2911
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
3713
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
2912
3714
  renameSync(tmp, f);
2913
3715
  }
2914
3716
  key(ref) {
@@ -2919,22 +3721,44 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2919
3721
  return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
2920
3722
  }
2921
3723
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
2922
- const dir = path11.join(root, "locks");
2923
- mkdirSync5(dir, { recursive: true, mode: 448 });
2924
- const f = path11.join(dir, `${name}.lock`);
3724
+ const dir = path12.join(root, "locks");
3725
+ mkdirSync3(dir, { recursive: true, mode: 448 });
3726
+ const f = path12.join(dir, `${name}.lock`);
3727
+ const nonce = randomBytes2(8).toString("hex");
3728
+ const mine = `${process.pid} ${nonce}`;
3729
+ let body = null;
2925
3730
  try {
2926
- if (Date.now() - statSync5(f).mtimeMs > staleMs || !processAlive(Number(readFileSync8(f, "utf8")))) unlinkSync3(f);
2927
- else return null;
3731
+ body = readFileSync9(f, "utf8");
3732
+ const stale2 = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
3733
+ if (!stale2) return null;
2928
3734
  } catch {
3735
+ body = null;
3736
+ }
3737
+ if (body !== null) {
3738
+ const aside = `${f}.${process.pid}.${nonce}.stale`;
3739
+ try {
3740
+ renameSync(f, aside);
3741
+ } catch {
3742
+ return null;
3743
+ }
3744
+ const moved = readFileSync9(aside, "utf8");
3745
+ unlinkSync2(aside);
3746
+ if (moved !== body) {
3747
+ try {
3748
+ writeFileSync3(f, moved, { flag: "wx" });
3749
+ } catch {
3750
+ }
3751
+ return null;
3752
+ }
2929
3753
  }
2930
3754
  try {
2931
- writeFileSync6(f, String(process.pid), { flag: "wx" });
3755
+ writeFileSync3(f, mine, { flag: "wx" });
2932
3756
  } catch {
2933
3757
  return null;
2934
3758
  }
2935
3759
  return () => {
2936
3760
  try {
2937
- unlinkSync3(f);
3761
+ if (readFileSync9(f, "utf8") === mine) unlinkSync2(f);
2938
3762
  } catch {
2939
3763
  }
2940
3764
  };
@@ -2953,7 +3777,7 @@ function processAlive(pid) {
2953
3777
  import { createHash as createHash3 } from "node:crypto";
2954
3778
  var RULESET_PII = "pii@1";
2955
3779
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
2956
- var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
3780
+ var PHONE = /(?<![\w.])(?:(?<!\)[ .-]?|\+\d{1,3}[ .-]?)(\+?(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))(?:(?= (?:[01]?\d|2[0-3]):)|(?!\w|[ .-]\d\d))|(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w))/g;
2957
3781
  function looksPhone(s) {
2958
3782
  const digits = s.replace(/\D/g, "");
2959
3783
  if (digits.length < 7 || digits.length > 15) return false;
@@ -2964,7 +3788,7 @@ function looksPhone(s) {
2964
3788
  function reducePii(text, home) {
2965
3789
  if (!text) return text;
2966
3790
  let out = text.replace(EMAIL, "[email]");
2967
- out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
3791
+ out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
2968
3792
  if (home) {
2969
3793
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2970
3794
  out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -3161,6 +3985,8 @@ var TYPE_TIER = {
3161
3985
  "subagent.ended": 1,
3162
3986
  "context.compacted": 1,
3163
3987
  "limits.observed": 1,
3988
+ "delegation.started": 0,
3989
+ "delegation.ended": 0,
3164
3990
  native: 3
3165
3991
  };
3166
3992
  var DATA_ALLOW = {
@@ -3171,17 +3997,19 @@ var DATA_ALLOW = {
3171
3997
  "session.forgotten": { 0: [] },
3172
3998
  "turn.started": { 0: ["turn_id", "submitted_by", "injected"], 1: ["prompt_chars"] },
3173
3999
  "turn.ended": { 0: ["turn_id", "reason", "ms", "background"], 1: ["error"] },
3174
- "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
3175
- "turn.progress": { 2: ["turn", "user", "assistant", "at"] },
4000
+ "turn.transcript": { 2: ["turn", "user", "assistant", "at", "injected"] },
4001
+ "turn.progress": { 2: ["turn", "user", "assistant", "at", "injected"] },
3176
4002
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
3177
4003
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
3178
4004
  "files.changed": { 1: ["paths"] },
3179
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
4005
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
3180
4006
  "attention.cleared": { 0: ["attention_id", "how"] },
3181
4007
  "subagent.started": { 1: ["agent_id", "agent_type"] },
3182
4008
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
3183
4009
  "context.compacted": { 1: ["trigger"] },
3184
- "limits.observed": { 1: ["windows", "plan", "account_id"] }
4010
+ "limits.observed": { 1: ["windows", "plan", "account_id"] },
4011
+ "delegation.started": { 0: ["ask_id", "via", "harness", "to"], 1: ["text"] },
4012
+ "delegation.ended": { 0: ["ask_id", "to", "ok", "ms", "error", "relayed"], 2: ["reply"] }
3185
4013
  };
3186
4014
  var SESSION_ALLOW = [
3187
4015
  "id",
@@ -3197,7 +4025,8 @@ var SESSION_ALLOW = [
3197
4025
  "account_id",
3198
4026
  "attended",
3199
4027
  "parent_id",
3200
- "launched_by"
4028
+ "launched_by",
4029
+ "delegated_by"
3201
4030
  ];
3202
4031
  var MAX_SIDE = 2e4;
3203
4032
  var MAX_IO = 64 * 1024;
@@ -3236,6 +4065,8 @@ function filterEvent(event, tier, pii, opts = {}) {
3236
4065
  if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
3237
4066
  if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
3238
4067
  if (typeof data.message === "string") data.message = cap(data.message, 200);
4068
+ if (event.type === "delegation.started" && typeof data.text === "string") data.text = cap(data.text, 200);
4069
+ if (event.type === "delegation.ended" && typeof data.reply === "string") data.reply = cap(data.reply, MAX_SIDE);
3239
4070
  const rulesets = [];
3240
4071
  let out = { ...event, tier, session, data, privacy: { rulesets, pii } };
3241
4072
  if (tier > 0) {
@@ -3262,8 +4093,8 @@ function shortId(harness, id) {
3262
4093
  }
3263
4094
 
3264
4095
  // ../core/src/sinks/file.ts
3265
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3266
- import path12 from "node:path";
4096
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
4097
+ import path13 from "node:path";
3267
4098
  var FileSink = class {
3268
4099
  constructor(name, file, tier, pii) {
3269
4100
  this.name = name;
@@ -3277,7 +4108,7 @@ var FileSink = class {
3277
4108
  pii;
3278
4109
  async send(events) {
3279
4110
  try {
3280
- mkdirSync6(path12.dirname(this.file), { recursive: true });
4111
+ mkdirSync4(path13.dirname(this.file), { recursive: true });
3281
4112
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3282
4113
  `);
3283
4114
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3314,24 +4145,24 @@ var StdoutSink = class {
3314
4145
  import {
3315
4146
  appendFileSync as appendFileSync3,
3316
4147
  existsSync as existsSync10,
3317
- mkdirSync as mkdirSync8,
3318
- readdirSync as readdirSync6,
3319
- readFileSync as readFileSync10,
3320
- renameSync as renameSync3,
3321
- statSync as statSync6,
3322
- unlinkSync as unlinkSync4,
3323
- writeFileSync as writeFileSync8
4148
+ mkdirSync as mkdirSync7,
4149
+ readdirSync as readdirSync7,
4150
+ readFileSync as readFileSync12,
4151
+ renameSync as renameSync4,
4152
+ statSync as statSync7,
4153
+ unlinkSync as unlinkSync3,
4154
+ writeFileSync as writeFileSync6
3324
4155
  } from "node:fs";
3325
- import os3 from "node:os";
3326
- import path15 from "node:path";
4156
+ import os4 from "node:os";
4157
+ import path17 from "node:path";
3327
4158
 
3328
4159
  // src/control/aliases.ts
3329
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
3330
- import path13 from "node:path";
4160
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
4161
+ import path14 from "node:path";
3331
4162
 
3332
4163
  // src/secrets.ts
3333
4164
  import { spawnSync } from "node:child_process";
3334
- import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
4165
+ import { createHash as createHash5, randomBytes as randomBytes3 } from "node:crypto";
3335
4166
  var SERVICE = "sessionpipe";
3336
4167
  var defaultRunner = (cmd, args, input) => {
3337
4168
  const r = spawnSync(cmd, args, {
@@ -3355,9 +4186,9 @@ function secretGet(store, account, run = defaultRunner) {
3355
4186
  function sinkWithSecrets(s, run = defaultRunner) {
3356
4187
  if (!s.token_in) return s;
3357
4188
  const token = secretGet(s.token_in, sinkAccount(s.name), run);
3358
- if (!token) return null;
3359
4189
  const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
3360
- return { ...s, token, ...secret ? { secret } : {} };
4190
+ if (!token && !secret) return null;
4191
+ return { ...s, ...token ? { token } : {}, ...secret ? { secret } : {} };
3361
4192
  }
3362
4193
 
3363
4194
  // src/control/store.ts
@@ -3371,10 +4202,10 @@ function startOptionsOf(v) {
3371
4202
  }
3372
4203
 
3373
4204
  // src/control/aliases.ts
3374
- var aliasFile = (controlDir) => path13.join(controlDir, "aliases.json");
4205
+ var aliasFile = (controlDir) => path14.join(controlDir, "aliases.json");
3375
4206
  function readAliases(file) {
3376
4207
  try {
3377
- const j = JSON.parse(readFileSync9(file, "utf8"));
4208
+ const j = JSON.parse(readFileSync10(file, "utf8"));
3378
4209
  if (!j || typeof j !== "object" || Array.isArray(j)) return {};
3379
4210
  const out = {};
3380
4211
  for (const [k, v] of Object.entries(j)) {
@@ -3395,12 +4226,12 @@ function receiverIdOf(all, harness, id) {
3395
4226
 
3396
4227
  // src/control/local.ts
3397
4228
  import net from "node:net";
3398
- import os2 from "node:os";
3399
- import path14 from "node:path";
4229
+ import os3 from "node:os";
4230
+ import path15 from "node:path";
3400
4231
  function socketPath(state, env = process.env) {
3401
4232
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3402
- if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
3403
- return path14.join(state, "control.sock");
4233
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os3.userInfo().username}`;
4234
+ return path15.join(state, "control.sock");
3404
4235
  }
3405
4236
  function ask(sock, req, timeoutMs) {
3406
4237
  return new Promise((resolve) => {
@@ -3432,6 +4263,76 @@ function ask(sock, req, timeoutMs) {
3432
4263
  });
3433
4264
  }
3434
4265
 
4266
+ // src/fold.ts
4267
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
4268
+ import path16 from "node:path";
4269
+ var TOOLS_MODES = ["all", "main", "beat"];
4270
+ var TOOL_FOLD_MS = 6e4;
4271
+ var KEEP_MS = 24 * 60 * 6e4;
4272
+ function isToolsMode(v) {
4273
+ return typeof v === "string" && TOOLS_MODES.includes(v);
4274
+ }
4275
+ function toolsMode(s) {
4276
+ if (isToolsMode(s.tools)) return s.tools;
4277
+ if (isToolsMode(s.receiver_tools)) return s.receiver_tools;
4278
+ return "all";
4279
+ }
4280
+ var agentOf = (e) => typeof e.data.agent_id === "string" ? e.data.agent_id : "";
4281
+ var thins = (mode, agent) => mode === "beat" || mode === "main" && agent !== "";
4282
+ var askKey = (key, agent) => `${key}\0ask\0${agent}`;
4283
+ function sendTool(e, mode, clock, key) {
4284
+ if (mode === "all") return true;
4285
+ const agent = agentOf(e);
4286
+ const t = Date.parse(e.time);
4287
+ if (e.type === "attention.needed") {
4288
+ if (thins(mode, agent)) clock[askKey(key, agent)] = Number.isFinite(t) ? t : 0;
4289
+ return true;
4290
+ }
4291
+ if (e.type !== "tool.started" && e.type !== "tool.ended" || !thins(mode, agent)) return true;
4292
+ if (e.type === "tool.started") return false;
4293
+ if (!Number.isFinite(t)) return true;
4294
+ const ask2 = askKey(key, agent);
4295
+ const asked = clock[ask2];
4296
+ if (asked !== void 0 && t >= asked) {
4297
+ delete clock[ask2];
4298
+ clock[key] = t;
4299
+ return true;
4300
+ }
4301
+ const last = clock[key];
4302
+ if (last !== void 0 && t > last - TOOL_FOLD_MS && t < last + TOOL_FOLD_MS) return false;
4303
+ clock[key] = t;
4304
+ return true;
4305
+ }
4306
+ var ToolClock = class {
4307
+ constructor(root, sink) {
4308
+ this.root = root;
4309
+ this.sink = sink;
4310
+ }
4311
+ root;
4312
+ sink;
4313
+ file() {
4314
+ return path16.join(this.root, "folds", `${safeId(this.sink)}.json`);
4315
+ }
4316
+ read() {
4317
+ try {
4318
+ const j = JSON.parse(readFileSync11(this.file(), "utf8"));
4319
+ return j && typeof j === "object" && !Array.isArray(j) ? j : {};
4320
+ } catch {
4321
+ return {};
4322
+ }
4323
+ }
4324
+ write(c) {
4325
+ const newest = Math.max(0, ...Object.values(c).filter(Number.isFinite));
4326
+ const kept = {};
4327
+ for (const [k, v] of Object.entries(c)) if (Number.isFinite(v) && v >= newest - KEEP_MS) kept[k] = v;
4328
+ const f = this.file();
4329
+ mkdirSync6(path16.dirname(f), { recursive: true, mode: 448 });
4330
+ const tmp = `${f}.${process.pid}.tmp`;
4331
+ writeFileSync5(tmp, JSON.stringify(kept), { mode: 384 });
4332
+ renameSync3(tmp, f);
4333
+ }
4334
+ };
4335
+
3435
4336
  // src/http-sink.ts
3436
4337
  import { createHash as createHash6, createHmac } from "node:crypto";
3437
4338
  var TIMEOUT_MS = 15e3;
@@ -3531,6 +4432,11 @@ var HttpSink = class {
3531
4432
  async function tellDaemon(state, job, session, cwd, transcript) {
3532
4433
  const sock = socketPath(state);
3533
4434
  if (process.platform !== "win32" && !existsSync10(sock)) return;
4435
+ let mode = null;
4436
+ try {
4437
+ mode = claude_code_control_exports.hookMode(JSON.parse(job.stdin));
4438
+ } catch {
4439
+ }
3534
4440
  await ask(
3535
4441
  sock,
3536
4442
  {
@@ -3538,7 +4444,8 @@ async function tellDaemon(state, job, session, cwd, transcript) {
3538
4444
  session: `${job.harness}:${session}`,
3539
4445
  event: job.event,
3540
4446
  ...cwd ? { cwd } : {},
3541
- ...transcript ? { transcript } : {}
4447
+ ...transcript ? { transcript } : {},
4448
+ ...mode ? { mode } : {}
3542
4449
  },
3543
4450
  300
3544
4451
  );
@@ -3546,20 +4453,20 @@ async function tellDaemon(state, job, session, cwd, transcript) {
3546
4453
  var MAX_TRIES = 10;
3547
4454
  function enqueueJob(state, job) {
3548
4455
  const jobs = jobsDir(state);
3549
- mkdirSync8(jobs, { recursive: true, mode: 448 });
3550
- const f = path15.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3551
- writeFileSync8(f, JSON.stringify(job), { mode: 384 });
4456
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
4457
+ const f = path17.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4458
+ writeFileSync6(f, JSON.stringify(job), { mode: 384 });
3552
4459
  return f;
3553
4460
  }
3554
4461
  function claimJob(file) {
3555
4462
  const claimed = `${file}.${process.pid}.running`;
3556
4463
  try {
3557
- renameSync3(file, claimed);
4464
+ renameSync4(file, claimed);
3558
4465
  } catch {
3559
4466
  return null;
3560
4467
  }
3561
4468
  try {
3562
- return { job: JSON.parse(readFileSync10(claimed, "utf8")), file: claimed };
4469
+ return { job: JSON.parse(readFileSync12(claimed, "utf8")), file: claimed };
3563
4470
  } catch {
3564
4471
  releaseJob(claimed);
3565
4472
  return null;
@@ -3568,15 +4475,15 @@ function claimJob(file) {
3568
4475
  function releaseJob(claimed) {
3569
4476
  if (!claimed) return;
3570
4477
  try {
3571
- unlinkSync4(claimed);
4478
+ unlinkSync3(claimed);
3572
4479
  } catch {
3573
4480
  }
3574
4481
  }
3575
- async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4) {
4482
+ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4, o = {}) {
3576
4483
  const jobs = jobsDir(state);
3577
4484
  let names = [];
3578
4485
  try {
3579
- names = readdirSync6(jobs);
4486
+ names = readdirSync7(jobs);
3580
4487
  } catch {
3581
4488
  return 0;
3582
4489
  }
@@ -3584,40 +4491,43 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
3584
4491
  for (const n of names) {
3585
4492
  const m = /^(.*\.json)\.(\d+)\.running$/.exec(n);
3586
4493
  if (!m) continue;
3587
- const f = path15.join(jobs, n);
4494
+ const f = path17.join(jobs, n);
3588
4495
  try {
3589
- if (processAlive(Number(m[2])) && Date.now() - statSync6(f).mtimeMs < abandonedMs) continue;
3590
- renameSync3(f, path15.join(jobs, m[1]));
4496
+ if (processAlive(Number(m[2])) && Date.now() - statSync7(f).mtimeMs < abandonedMs) continue;
4497
+ renameSync4(f, path17.join(jobs, m[1]));
3591
4498
  queued.push(m[1]);
3592
4499
  } catch {
3593
4500
  }
3594
4501
  }
3595
4502
  let ran = 0;
3596
4503
  for (const n of queued.sort().slice(0, limit)) {
3597
- const f = path15.join(jobs, n);
4504
+ if (o.deadline !== void 0 && Date.now() + SWEEP_LOCK_WAIT_MS + PASS_BUDGET_MS > o.deadline) break;
4505
+ const f = path17.join(jobs, n);
3598
4506
  try {
3599
- if (Date.now() - statSync6(f).mtimeMs < olderThanMs) continue;
4507
+ if (Date.now() - statSync7(f).mtimeMs < olderThanMs) continue;
3600
4508
  } catch {
3601
4509
  continue;
3602
4510
  }
3603
4511
  const c = claimJob(f);
3604
4512
  if (!c) continue;
3605
4513
  try {
3606
- await runJob(c.job, { state, claimed: c.file });
4514
+ const r = await runJob(c.job, { state, claimed: c.file, drain: false, lockWaitMs: SWEEP_LOCK_WAIT_MS });
4515
+ if (r.events.length) markMore(state);
3607
4516
  } catch {
3608
4517
  } finally {
3609
4518
  releaseJob(c.file);
3610
4519
  }
3611
4520
  ran++;
3612
4521
  }
4522
+ if (ran) await drain(state, readConfig().sinks, new Outbox(state), void 0, o);
3613
4523
  return ran;
3614
4524
  }
3615
4525
  function log(state, line) {
3616
4526
  try {
3617
- mkdirSync8(state, { recursive: true, mode: 448 });
3618
- const f = path15.join(state, "log");
4527
+ mkdirSync7(state, { recursive: true, mode: 448 });
4528
+ const f = path17.join(state, "log");
3619
4529
  try {
3620
- if (statSync6(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
4530
+ if (statSync7(f).size > 1024 * 1024) renameSync4(f, `${f}.1`);
3621
4531
  } catch {
3622
4532
  }
3623
4533
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3626,11 +4536,11 @@ function log(state, line) {
3626
4536
  }
3627
4537
  }
3628
4538
  function factsState(state) {
3629
- const file = (h, s) => path15.join(state, "facts", safeId(h), `${safeId(s)}.json`);
4539
+ const file = (h, s) => path17.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3630
4540
  return {
3631
4541
  get(h, s) {
3632
4542
  try {
3633
- return JSON.parse(readFileSync10(file(h, s), "utf8"));
4543
+ return JSON.parse(readFileSync12(file(h, s), "utf8"));
3634
4544
  } catch {
3635
4545
  return {};
3636
4546
  }
@@ -3639,8 +4549,8 @@ function factsState(state) {
3639
4549
  const cur = this.get(h, s);
3640
4550
  const next = { ...cur, ...patch };
3641
4551
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
3642
- mkdirSync8(path15.dirname(file(h, s)), { recursive: true, mode: 448 });
3643
- writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
4552
+ mkdirSync7(path17.dirname(file(h, s)), { recursive: true, mode: 448 });
4553
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
3644
4554
  }
3645
4555
  };
3646
4556
  }
@@ -3653,10 +4563,10 @@ function buildSinks(sinks, o = {}) {
3653
4563
  o.locked?.(raw.name);
3654
4564
  continue;
3655
4565
  }
3656
- const tier = Math.min(s.tier, s.max_tier ?? 3);
4566
+ const tier = Math.trunc(Math.min(Number(s.tier) || 0, s.max_tier ?? 3));
3657
4567
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
3658
4568
  else if (s.url.startsWith("file:"))
3659
- out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os3.homedir()), tier, !!s.pii));
4569
+ out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os4.homedir()), tier, !!s.pii));
3660
4570
  else if (/^https?:\/\//.test(s.url)) out.push(new HttpSink(s, tier));
3661
4571
  }
3662
4572
  return out;
@@ -3664,13 +4574,14 @@ function buildSinks(sinks, o = {}) {
3664
4574
  var PROGRESS_MS = 2e4;
3665
4575
  function openTurn(read, o) {
3666
4576
  const last = read.turns[read.turns.length - 1];
3667
- const fresh = (user, at) => ({
4577
+ const fresh = (user, at, injected) => ({
3668
4578
  turn: o.base + read.turns.length,
3669
4579
  user: user.slice(0, MAX_TURN_SIDE),
3670
4580
  assistant: "",
3671
- at
4581
+ at,
4582
+ ...injected ? { injected: true } : {}
3672
4583
  });
3673
- if (read.open) return fresh(read.open.user, read.open.at);
4584
+ if (read.open) return fresh(read.open.user, read.open.at, read.open.injected);
3674
4585
  const prompt = o.prompt?.trim();
3675
4586
  if (prompt && !isInjected(prompt) && !(last && last.user.includes(prompt.slice(0, 200)))) return fresh(prompt, o.at);
3676
4587
  if (!last) return null;
@@ -3678,6 +4589,7 @@ function openTurn(read, o) {
3678
4589
  turn: o.base + read.turns.length - 1,
3679
4590
  user: last.user.slice(0, MAX_TURN_SIDE),
3680
4591
  assistant: capTail(last.assistant, MAX_TURN_SIDE),
4592
+ ...last.injected ? { injected: true } : {},
3681
4593
  at: last.at
3682
4594
  };
3683
4595
  }
@@ -3685,6 +4597,7 @@ var MAX_TURN_SIDE = 2e4;
3685
4597
  var JOB_OWN_ENV = [
3686
4598
  "CLAUDE_CODE_SESSION_ATTENDED",
3687
4599
  "CLAUDE_CODE_HOST_SESSION_ID",
4600
+ "SESSIONPIPE_ASKED_BY",
3688
4601
  "SESSIONPIPE_CONTROL_JOB",
3689
4602
  "SESSIONPIPE_LAUNCHED_BY"
3690
4603
  ];
@@ -3712,7 +4625,7 @@ async function runJob(job, opts = {}) {
3712
4625
  );
3713
4626
  return { events: [], delivered: {} };
3714
4627
  }
3715
- const id = receiverIdOf(readAliases(aliasFile(path15.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
4628
+ const id = receiverIdOf(readAliases(aliasFile(path17.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3716
4629
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
3717
4630
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
3718
4631
  const outbox = new Outbox(state);
@@ -3749,7 +4662,7 @@ async function runJob(job, opts = {}) {
3749
4662
  learned = rest;
3750
4663
  if (harness_version) facts.save(job.harness, id, { harness_version });
3751
4664
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
3752
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path15.isAbsolute(r.session.cwd))
4665
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path17.isAbsolute(r.session.cwd))
3753
4666
  facts.save(job.harness, id, { cwd: r.session.cwd });
3754
4667
  }
3755
4668
  const known = mem.session ?? {};
@@ -3762,10 +4675,11 @@ async function runJob(job, opts = {}) {
3762
4675
  ...learned.branch ? { branch: learned.branch } : {},
3763
4676
  id,
3764
4677
  seq: 0,
3765
- machine: machineName(cfg, os3.hostname()),
4678
+ machine: machineName(cfg, os4.hostname()),
3766
4679
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
3767
4680
  ...r.session.model ? { model: r.session.model } : {},
3768
- ...r.session.launched_by ? { launched_by: r.session.launched_by } : {}
4681
+ ...r.session.launched_by ? { launched_by: r.session.launched_by } : {},
4682
+ ...r.session.delegated_by ? { delegated_by: r.session.delegated_by } : {}
3769
4683
  };
3770
4684
  const version = facts.get(job.harness, id).harness_version ?? void 0;
3771
4685
  for (const a of r.events) {
@@ -3819,7 +4733,13 @@ async function runJob(job, opts = {}) {
3819
4733
  {
3820
4734
  type: "turn.transcript",
3821
4735
  harnessEvent: job.event,
3822
- data: { turn: turnNo++, user: t.user, assistant: t.assistant, at: new Date(t.at).toISOString() }
4736
+ data: {
4737
+ turn: turnNo++,
4738
+ user: t.user,
4739
+ assistant: t.assistant,
4740
+ at: new Date(t.at).toISOString(),
4741
+ ...t.injected ? { injected: true } : {}
4742
+ }
3823
4743
  },
3824
4744
  {
3825
4745
  harness: { name: job.harness, ...version ? { version } : {} },
@@ -3860,29 +4780,84 @@ async function runJob(job, opts = {}) {
3860
4780
  } finally {
3861
4781
  release();
3862
4782
  }
3863
- const delivered = await drain(state, cfg.sinks, outbox, ref) ?? {};
3864
4783
  if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
4784
+ const delivered = opts.drain === false ? {} : await drain(state, cfg.sinks, outbox, ref, {
4785
+ ...opts.clock ? { clock: opts.clock } : {},
4786
+ ...opts.deadline !== void 0 ? { deadline: opts.deadline } : {},
4787
+ ...opts.handoff ? { handoff: opts.handoff } : {}
4788
+ }) ?? {};
3865
4789
  return { events, delivered };
3866
4790
  }
3867
- async function drain(state, sinkConfigs, outbox, first) {
3868
- const more = path15.join(state, "locks", "flush.more");
4791
+ var REAL_CLOCK = { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
4792
+ var PACE_MS = 1e3;
4793
+ var sentFile = (state) => path17.join(state, "locks", "flush.sent");
4794
+ function lastSent(state) {
4795
+ try {
4796
+ return Number(readFileSync12(sentFile(state), "utf8"));
4797
+ } catch {
4798
+ return 0;
4799
+ }
4800
+ }
4801
+ function noteSent(state, at) {
4802
+ try {
4803
+ mkdirSync7(path17.dirname(sentFile(state)), { recursive: true, mode: 448 });
4804
+ writeFileSync6(sentFile(state), String(at), { mode: 384 });
4805
+ } catch {
4806
+ }
4807
+ }
4808
+ function waitingForReceiver(state, sinkConfigs, outbox, now) {
4809
+ const receivers = sinkConfigs.filter(
4810
+ (s) => !s.paused && /^https?:\/\//.test(s.url) && !((readBackoff(state, s.name)?.until ?? 0) > now)
4811
+ );
4812
+ if (!receivers.length) return false;
4813
+ const refs = outbox.sessions();
4814
+ for (const s of receivers) {
4815
+ const cursors = new Cursors(state, s.name);
4816
+ const c = cursors.read();
4817
+ for (const ref of refs) {
4818
+ try {
4819
+ if (statSync7(outbox.file(ref)).size > (c[cursors.key(ref)] ?? 0)) return true;
4820
+ } catch {
4821
+ }
4822
+ }
4823
+ }
4824
+ return false;
4825
+ }
4826
+ var PASS_BUDGET_MS = 3e3;
4827
+ var SWEEP_LOCK_WAIT_MS = 1e3;
4828
+ var moreFile = (state) => path17.join(state, "locks", "flush.more");
4829
+ function markMore(state) {
4830
+ try {
4831
+ mkdirSync7(path17.dirname(moreFile(state)), { recursive: true, mode: 448 });
4832
+ writeFileSync6(moreFile(state), "", { mode: 384 });
4833
+ } catch {
4834
+ }
4835
+ }
4836
+ async function drain(state, sinkConfigs, outbox, first, o = {}) {
4837
+ const clock = o.clock ?? REAL_CLOCK;
4838
+ const more = moreFile(state);
3869
4839
  let release = takeNamedLock(state, "flush");
3870
4840
  if (!release) {
3871
- try {
3872
- writeFileSync8(more, "", { mode: 384 });
3873
- } catch {
3874
- }
4841
+ markMore(state);
3875
4842
  release = takeNamedLock(state, "flush");
3876
4843
  if (!release) return null;
3877
4844
  }
3878
4845
  const delivered = {};
3879
4846
  for (; ; ) {
3880
- try {
3881
- unlinkSync4(more);
3882
- } catch {
4847
+ if (o.deadline !== void 0 && clock.now() + PASS_BUDGET_MS > o.deadline) {
4848
+ markMore(state);
4849
+ release();
4850
+ o.handoff?.();
4851
+ return delivered;
3883
4852
  }
3884
4853
  try {
3885
- for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first)))
4854
+ const wait = Math.min(PACE_MS, lastSent(state) + PACE_MS - clock.now());
4855
+ if (wait > 0 && waitingForReceiver(state, sinkConfigs, outbox, clock.now())) await clock.sleep(wait);
4856
+ try {
4857
+ unlinkSync3(more);
4858
+ } catch {
4859
+ }
4860
+ for (const [k, v] of Object.entries(await flush(state, sinkConfigs, outbox, first, { clock })))
3886
4861
  delivered[k] = (delivered[k] ?? 0) + v;
3887
4862
  } finally {
3888
4863
  release();
@@ -3893,10 +4868,10 @@ async function drain(state, sinkConfigs, outbox, first) {
3893
4868
  }
3894
4869
  }
3895
4870
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
3896
- var backoffFile = (state, sink) => path15.join(state, "backoff", `${safeId(sink)}.json`);
4871
+ var backoffFile = (state, sink) => path17.join(state, "backoff", `${safeId(sink)}.json`);
3897
4872
  function readBackoff(state, sink) {
3898
4873
  try {
3899
- const b = JSON.parse(readFileSync10(backoffFile(state, sink), "utf8"));
4874
+ const b = JSON.parse(readFileSync12(backoffFile(state, sink), "utf8"));
3900
4875
  return typeof b.until === "number" ? b : null;
3901
4876
  } catch {
3902
4877
  return null;
@@ -3909,15 +4884,15 @@ function noteFailure(state, sink, res, prev) {
3909
4884
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
3910
4885
  const f = backoffFile(state, sink);
3911
4886
  try {
3912
- mkdirSync8(path15.dirname(f), { recursive: true, mode: 448 });
3913
- writeFileSync8(f, JSON.stringify(b), { mode: 384 });
4887
+ mkdirSync7(path17.dirname(f), { recursive: true, mode: 448 });
4888
+ writeFileSync6(f, JSON.stringify(b), { mode: 384 });
3914
4889
  } catch {
3915
4890
  }
3916
4891
  return b;
3917
4892
  }
3918
4893
  function clearBackoff(state, sink) {
3919
4894
  try {
3920
- unlinkSync4(backoffFile(state, sink));
4895
+ unlinkSync3(backoffFile(state, sink));
3921
4896
  } catch {
3922
4897
  }
3923
4898
  }
@@ -3927,14 +4902,31 @@ var BATCH = 50;
3927
4902
  var MAX_PER_RUN = 500;
3928
4903
  async function flush(state, sinkConfigs, outbox, first, o = {}) {
3929
4904
  const delivered = {};
4905
+ const clock = o.clock ?? REAL_CLOCK;
3930
4906
  const sinks = buildSinks(sinkConfigs, {
3931
4907
  locked: (name) => log(state, `sink ${name}: its token is in a keychain this process can't read; left for later`)
3932
4908
  });
4909
+ const modes = new Map(sinkConfigs.map((s) => [s.name, toolsMode(s)]));
3933
4910
  const refs = outbox.sessions();
3934
4911
  if (first) {
3935
4912
  const k = `${first.harness}/${safeId(first.session)}`;
3936
4913
  refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
3937
4914
  }
4915
+ const unreadable = /* @__PURE__ */ new Set();
4916
+ const readOrSkip = (ref, offset, room) => {
4917
+ try {
4918
+ return outbox.read(ref, offset, room);
4919
+ } catch (e) {
4920
+ const code = e.code;
4921
+ if (typeof code !== "string") throw e;
4922
+ const f = outbox.file(ref);
4923
+ if (!unreadable.has(f)) {
4924
+ unreadable.add(f);
4925
+ log(state, `drain: can't read ${f} (${code}); skipped, its cursors stay`);
4926
+ }
4927
+ return null;
4928
+ }
4929
+ };
3938
4930
  const drainSink = async (sink) => {
3939
4931
  let backoff = readBackoff(state, sink.name);
3940
4932
  if (backoff && !o.force && backoff.until > Date.now()) {
@@ -3943,11 +4935,16 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3943
4935
  }
3944
4936
  const cursors = new Cursors(state, sink.name);
3945
4937
  const c = cursors.read();
4938
+ const mode = modes.get(sink.name);
4939
+ const tools = new ToolClock(state, sink.name);
4940
+ let clockNow = mode === "all" ? {} : tools.read();
4941
+ let clockNext = { ...clockNow };
3946
4942
  let sent = 0;
3947
4943
  let stalled = false;
3948
4944
  let batch = [];
3949
4945
  let pending = {};
3950
4946
  const deliver = async (events) => {
4947
+ if (sink instanceof HttpSink) noteSent(state, clock.now());
3951
4948
  const res = await sink.send(events);
3952
4949
  if (res.ok || res.action !== "split") return res;
3953
4950
  if (events.length === 1) {
@@ -3955,10 +4952,20 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3955
4952
  log(state, `sink ${sink.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
3956
4953
  return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
3957
4954
  }
4955
+ const part = async (some) => {
4956
+ const r = await deliver(some);
4957
+ if (r.ok || r.action !== "drop") return r;
4958
+ log(
4959
+ state,
4960
+ `sink ${sink.name}: ${r.status ?? ""} ${r.error ?? ""} \u2192 drop, ${some.length} of ${events.length} events`
4961
+ );
4962
+ const rejected = some.map((e) => ({ id: e.id, reason: "dropped" }));
4963
+ return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected };
4964
+ };
3958
4965
  const half = Math.ceil(events.length / 2);
3959
- const a = await deliver(events.slice(0, half));
4966
+ const a = await part(events.slice(0, half));
3960
4967
  if (!a.ok) return a;
3961
- const b = await deliver(events.slice(half));
4968
+ const b = await part(events.slice(half));
3962
4969
  if (!b.ok) return b;
3963
4970
  return {
3964
4971
  ok: true,
@@ -3968,11 +4975,17 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3968
4975
  rejected: [...a.rejected ?? [], ...b.rejected ?? []]
3969
4976
  };
3970
4977
  };
4978
+ const commitClock = () => {
4979
+ if (mode === "all" || JSON.stringify(clockNext) === JSON.stringify(clockNow)) return;
4980
+ tools.write(clockNext);
4981
+ clockNow = { ...clockNext };
4982
+ };
3971
4983
  const send = async () => {
3972
4984
  if (!batch.length) {
3973
4985
  Object.assign(c, pending);
3974
4986
  pending = {};
3975
4987
  cursors.write(c);
4988
+ commitClock();
3976
4989
  return true;
3977
4990
  }
3978
4991
  const res = await deliver(batch);
@@ -3988,6 +5001,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3988
5001
  if (res.action !== "drop") {
3989
5002
  batch = [];
3990
5003
  pending = {};
5004
+ clockNext = { ...clockNow };
3991
5005
  return false;
3992
5006
  }
3993
5007
  } else if (backoff) {
@@ -3997,6 +5011,7 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
3997
5011
  sent += batch.length;
3998
5012
  Object.assign(c, pending);
3999
5013
  cursors.write(c);
5014
+ commitClock();
4000
5015
  batch = [];
4001
5016
  pending = {};
4002
5017
  return true;
@@ -4006,11 +5021,15 @@ async function flush(state, sinkConfigs, outbox, first, o = {}) {
4006
5021
  let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
4007
5022
  for (; ; ) {
4008
5023
  const room = BATCH - batch.length;
4009
- const { events, offset: next } = outbox.read(ref, offset, room);
5024
+ const read = readOrSkip(ref, offset, room);
5025
+ if (!read) break;
5026
+ const { events, offset: next } = read;
4010
5027
  if (!events.length) break;
4011
5028
  for (const e of events) {
4012
- const f = filterEvent(e, sink.tier, sink.pii, { home: os3.homedir() });
4013
- if (f) batch.push(f);
5029
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os4.homedir() });
5030
+ if (!f) continue;
5031
+ if (sendTool(f, mode, clockNext, cursors.key(ref))) batch.push(f);
5032
+ else if (o.thinned) o.thinned[sink.name] = (o.thinned[sink.name] ?? 0) + 1;
4014
5033
  }
4015
5034
  offset = next;
4016
5035
  pending[cursors.key(ref)] = offset;
@@ -4042,31 +5061,105 @@ function applySinkVerdict(name, res) {
4042
5061
  } catch {
4043
5062
  }
4044
5063
  }
4045
- var jobsDir = (state) => path15.join(state, "jobs");
5064
+ var jobsDir = (state) => path17.join(state, "jobs");
5065
+
5066
+ // src/wellknown.ts
5067
+ var WELL_KNOWN_MAX_AGE_MS = 24 * 60 * 6e4;
5068
+ var wellKnownUrl = (sinkUrl) => `${sinkUrl.replace(/\/$/, "")}/.well-known/sessionpipe`;
5069
+ async function readWellKnown(sinkUrl, timeoutMs, fetchFn = fetch) {
5070
+ const r = await fetchFn(wellKnownUrl(sinkUrl), { signal: AbortSignal.timeout(timeoutMs) });
5071
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
5072
+ return await r.json();
5073
+ }
5074
+ function applyWellKnown(s, wk, tier, now = /* @__PURE__ */ new Date()) {
5075
+ const notes = [];
5076
+ if (typeof wk.max_tier === "number") {
5077
+ s.max_tier = wk.max_tier;
5078
+ if (wk.max_tier < tier)
5079
+ notes.push(`receiver's max_tier is ${wk.max_tier}: sending tier ${wk.max_tier}, not ${tier}`);
5080
+ }
5081
+ if (wk.endpoints) s.endpoints = wk.endpoints;
5082
+ const before = s.receiver_tools;
5083
+ if (isToolsMode(wk.tools)) s.receiver_tools = wk.tools;
5084
+ else delete s.receiver_tools;
5085
+ if (s.receiver_tools !== before && s.receiver_tools && s.receiver_tools !== "all" && !isToolsMode(s.tools))
5086
+ notes.push(`the receiver asks for tool events \`${s.receiver_tools}\` (--tools all keeps every one)`);
5087
+ s.well_known_at = now.toISOString();
5088
+ s.well_known_checked_at = s.well_known_at;
5089
+ return notes;
5090
+ }
5091
+ var stale = (s, now) => {
5092
+ const last = Date.parse(s.well_known_checked_at ?? s.well_known_at ?? "");
5093
+ return !Number.isFinite(last) || now - last >= WELL_KNOWN_MAX_AGE_MS;
5094
+ };
5095
+ async function refreshWellKnown(cfg, o = {}) {
5096
+ const now = o.now ?? Date.now;
5097
+ let changed = false;
5098
+ for (const s of cfg.sinks) {
5099
+ if (!/^https?:\/\//.test(s.url) || !stale(s, now())) continue;
5100
+ const at = new Date(now());
5101
+ try {
5102
+ applyWellKnown(s, await readWellKnown(s.url, o.timeoutMs ?? 3e3, o.fetchFn), s.tier, at);
5103
+ } catch {
5104
+ s.well_known_checked_at = at.toISOString();
5105
+ }
5106
+ changed = true;
5107
+ }
5108
+ return changed;
5109
+ }
5110
+ async function refreshSinks(o = {}) {
5111
+ try {
5112
+ const cfg = readConfig();
5113
+ if (await refreshWellKnown(cfg, o)) writeConfig(cfg);
5114
+ } catch {
5115
+ }
5116
+ }
4046
5117
 
4047
5118
  // src/worker.ts
5119
+ var WORKER_LIFE_MS = 15e3;
5120
+ var DRAIN_ARG = "--drain";
5121
+ function startWorker(script, arg) {
5122
+ const p = spawn2(process.execPath, [script, arg], { detached: true, stdio: "ignore", windowsHide: true });
5123
+ p.unref();
5124
+ }
4048
5125
  async function workerMain(argv) {
4049
5126
  const jobFile = argv[2];
4050
5127
  if (!jobFile) return;
4051
- const claimed = claimJob(jobFile);
4052
- if (!claimed) return;
4053
- const deadline = setTimeout(() => process.exit(0), 15e3);
5128
+ const script = argv[1];
5129
+ let handedOff = false;
5130
+ const life = {
5131
+ deadline: Date.now() + WORKER_LIFE_MS,
5132
+ handoff: () => {
5133
+ if (handedOff) return;
5134
+ handedOff = true;
5135
+ startWorker(script, DRAIN_ARG);
5136
+ }
5137
+ };
5138
+ const deadline = setTimeout(() => process.exit(0), WORKER_LIFE_MS);
4054
5139
  deadline.unref();
4055
- await runJob(claimed.job, { claimed: claimed.file }).then((r) => {
5140
+ await refreshSinks();
5141
+ if (jobFile === DRAIN_ARG) {
5142
+ await drain(stateDir(), readConfig().sinks, new Outbox(stateDir()), void 0, life).then(() => sweepJobs(stateDir(), void 0, void 0, void 0, life)).catch(() => {
5143
+ }).finally(() => clearTimeout(deadline));
5144
+ return;
5145
+ }
5146
+ const claimed = claimJob(jobFile);
5147
+ if (!claimed) {
5148
+ clearTimeout(deadline);
5149
+ return;
5150
+ }
5151
+ await runJob(claimed.job, { claimed: claimed.file, ...life }).then((r) => {
4056
5152
  if (!r.requeued || (claimed.job.tries ?? 0) + 1 >= MAX_TRIES) return;
4057
- const p = spawn(process.execPath, [argv[1], r.requeued], {
4058
- detached: true,
4059
- stdio: "ignore",
4060
- windowsHide: true
4061
- });
4062
- p.unref();
5153
+ startWorker(script, r.requeued);
4063
5154
  }).catch(() => {
4064
- }).finally(() => releaseJob(claimed.file)).then(() => sweepJobs(stateDir())).catch(() => {
5155
+ }).finally(() => releaseJob(claimed.file)).then(() => sweepJobs(stateDir(), void 0, void 0, void 0, life)).catch(() => {
4065
5156
  }).finally(() => clearTimeout(deadline));
4066
5157
  }
4067
5158
  if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync2(process.argv[1])).href)
4068
5159
  workerMain(process.argv).finally(() => process.exit(0));
4069
5160
  export {
5161
+ DRAIN_ARG,
5162
+ WORKER_LIFE_MS,
4070
5163
  workerMain
4071
5164
  };
4072
5165
  //# sourceMappingURL=worker.js.map