sessionpipe-core 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 (39) hide show
  1. package/dist/control.js +26 -11
  2. package/dist/control.js.map +3 -3
  3. package/dist/index.js +550 -175
  4. package/dist/index.js.map +4 -4
  5. package/dist/schema/v1.d.ts +87 -1
  6. package/dist/schema/v1.d.ts.map +1 -1
  7. package/dist/schema.js +86 -11
  8. package/dist/schema.js.map +2 -2
  9. package/dist/service.js +1 -1
  10. package/dist/src/adapters/acp-control.d.ts.map +1 -1
  11. package/dist/src/adapters/antigravity.d.ts.map +1 -1
  12. package/dist/src/adapters/claude-code-control.d.ts +136 -6
  13. package/dist/src/adapters/claude-code-control.d.ts.map +1 -1
  14. package/dist/src/adapters/claude-code.d.ts.map +1 -1
  15. package/dist/src/adapters/claude-shaped.d.ts +10 -0
  16. package/dist/src/adapters/claude-shaped.d.ts.map +1 -1
  17. package/dist/src/adapters/codex.d.ts.map +1 -1
  18. package/dist/src/adapters/devin.d.ts +1 -1
  19. package/dist/src/adapters/devin.d.ts.map +1 -1
  20. package/dist/src/adapters/headless.d.ts +3 -1
  21. package/dist/src/adapters/headless.d.ts.map +1 -1
  22. package/dist/src/adapters/types.d.ts +8 -1
  23. package/dist/src/adapters/types.d.ts.map +1 -1
  24. package/dist/src/config.d.ts +15 -1
  25. package/dist/src/config.d.ts.map +1 -1
  26. package/dist/src/control/verify.d.ts +3 -0
  27. package/dist/src/control/verify.d.ts.map +1 -1
  28. package/dist/src/index.d.ts +4 -4
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/launched-by.d.ts +20 -3
  31. package/dist/src/launched-by.d.ts.map +1 -1
  32. package/dist/src/outbox.d.ts +35 -2
  33. package/dist/src/outbox.d.ts.map +1 -1
  34. package/dist/src/privacy/filter.d.ts.map +1 -1
  35. package/dist/src/privacy/pii.d.ts.map +1 -1
  36. package/dist/src/readers/transcripts.d.ts +11 -1
  37. package/dist/src/readers/transcripts.d.ts.map +1 -1
  38. package/dist/version.js +1 -1
  39. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe-core 0.12.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe-core 0.13.1 · Apache-2.0 · https://sessionpipe.org
2
2
  var __defProp = Object.defineProperty;
3
3
  var __export = (target, all) => {
4
4
  for (var name in all)
@@ -59,7 +59,7 @@ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
59
59
  // src/adapters/claude-code.ts
60
60
  import { execFileSync as execFileSync2 } from "node:child_process";
61
61
  import { createHash } from "node:crypto";
62
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
62
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
63
63
  import { userInfo } from "node:os";
64
64
  import path4 from "node:path";
65
65
 
@@ -211,7 +211,14 @@ ${m.text}`;
211
211
  continue;
212
212
  }
213
213
  if (cur) turns.push(cur);
214
- cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
214
+ cur = {
215
+ user: m.text,
216
+ assistant: "",
217
+ at: m.at,
218
+ startLine: m.line,
219
+ endLine: m.line,
220
+ ...m.injected ? { injected: true } : {}
221
+ };
215
222
  } else {
216
223
  if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
217
224
  cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
@@ -221,10 +228,17 @@ ${m.text}`;
221
228
  if (cur?.assistant) turns.push(cur);
222
229
  const line = cur && !cur.assistant ? cur.startLine : endLine;
223
230
  const out = {
224
- turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
231
+ turns: turns.map((t) => ({
232
+ user: t.user,
233
+ assistant: t.assistant,
234
+ at: t.at,
235
+ endLine: t.endLine,
236
+ ...t.injected ? { injected: true } : {}
237
+ })),
225
238
  line
226
239
  };
227
- if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
240
+ if (cur && !cur.assistant && cur.user)
241
+ out.open = { user: cur.user, at: cur.at, ...cur.injected ? { injected: true } : {} };
228
242
  return out;
229
243
  }
230
244
  var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
@@ -247,27 +261,51 @@ var textParts = (c, kinds) => {
247
261
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
248
262
  ).map((b) => b.text).join("\n");
249
263
  };
264
+ var SPOKEN = /* @__PURE__ */ new Set(["human", "peer"]);
265
+ var PEER_INTRO = /sent a message[^\n]*:$/;
266
+ var PEER_OUTRO = "\n\nThis came from another ";
267
+ var ENVELOPE = /^<cross-session-message\b[^>]*>\n([\s\S]*)\n<\/cross-session-message>$/;
268
+ function peerAsk(origin, text) {
269
+ const body = origin?.body;
270
+ let t = typeof body === "string" && body.trim() ? body : text;
271
+ if (t !== body) {
272
+ const nl = t.indexOf("\n");
273
+ if (nl > 0 && PEER_INTRO.test(t.slice(0, nl))) t = t.slice(nl + 1);
274
+ const outro = t.indexOf(PEER_OUTRO);
275
+ if (outro > 0) t = t.slice(0, outro);
276
+ }
277
+ return (ENVELOPE.exec(t.trim())?.[1] ?? t).trim();
278
+ }
250
279
  var queuedPrompt = (a) => {
251
280
  if (!a || typeof a !== "object") return "";
252
281
  const q = a;
253
282
  if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
254
- if (q.origin?.kind && q.origin.kind !== "human") return "";
255
- return textParts(q.prompt, ["text"]).trim();
283
+ const kind = q.origin?.kind ?? "human";
284
+ if (!SPOKEN.has(kind)) return "";
285
+ const text = textParts(q.prompt, ["text"]).trim();
286
+ return kind === "peer" ? peerAsk(q.origin, text) : text;
256
287
  };
288
+ var channelText = (text) => /^<channel\b[^>]*>([\s\S]*)<\/channel>$/.exec(text)?.[1]?.trim() ?? "";
257
289
  function readClaude(lines, from) {
258
290
  const msgs = [];
259
291
  for (let i = from; i < lines.length; i++) {
260
292
  const j = lines[i] ? parse(lines[i]) : null;
261
293
  if (!j || j.isSidechain) continue;
262
294
  const origin = j.origin;
263
- if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
264
- continue;
295
+ const channel = j.type === "user" && origin?.kind === "channel";
296
+ const peer = origin?.kind === "peer";
297
+ if (j.type === "user" && (j.isCompactSummary || j.repeatsCompactSummary)) continue;
298
+ if (j.type === "user" && !channel && !peer && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
265
299
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
266
300
  const c = j.message?.content;
267
- if (j.type === "user") {
268
- const text = textParts(c, ["text"]).trim();
301
+ if (channel) {
302
+ const text = channelText(textParts(c, ["text"]).trim());
303
+ if (text) msgs.push({ role: "user", text, at, line: i, injected: true });
304
+ } else if (j.type === "user") {
305
+ const raw = textParts(c, ["text"]).trim();
306
+ const text = peer ? peerAsk(origin, raw) : raw;
269
307
  if (!text || isInjected(text)) continue;
270
- msgs.push({ role: "user", text, at, line: i });
308
+ msgs.push({ role: "user", text, at, line: i, ...peer ? { injected: true } : {} });
271
309
  } else if (j.type === "assistant" && Array.isArray(c)) {
272
310
  const text = textParts(c, ["text"]).trim();
273
311
  if (text) msgs.push({ role: "assistant", text, at, line: i });
@@ -379,6 +417,41 @@ function readJson(file) {
379
417
  const f = readJsonFile(file);
380
418
  return f.ok ? f.value : {};
381
419
  }
420
+ function refused(file, e, what, existed) {
421
+ const { code, syscall } = e;
422
+ const why = `can't ${what} it (${code ?? String(e)})`;
423
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
424
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
425
+ return {
426
+ file,
427
+ changed: true,
428
+ skipped: true,
429
+ ...existed ? {} : { created: true },
430
+ note: `${why} part way; check the file before the harness reads it`
431
+ };
432
+ }
433
+ function writeBack(file, text, existed = true) {
434
+ try {
435
+ mkdirSync(path3.dirname(file), { recursive: true });
436
+ writeFileSync(file, text);
437
+ return null;
438
+ } catch (e) {
439
+ return refused(file, e, "write", existed);
440
+ }
441
+ }
442
+ function removeEmptied(file, empty) {
443
+ try {
444
+ unlinkSync(file);
445
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
446
+ } catch (e) {
447
+ const bad = refused(file, e, "remove", true);
448
+ return writeBack(file, empty) ?? {
449
+ file,
450
+ changed: true,
451
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
452
+ };
453
+ }
454
+ }
382
455
  function entryFor(event, harness, cmd, o) {
383
456
  const matcher = o.matcherFor(event);
384
457
  return {
@@ -420,8 +493,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
420
493
  }
421
494
  if (changed) {
422
495
  settings2.hooks = hooks;
423
- mkdirSync(path3.dirname(file), { recursive: true });
424
- writeFileSync(file, formatJson(settings2, f));
496
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
497
+ if (bad) return bad;
425
498
  }
426
499
  return { file, changed, created: !f.existed && changed };
427
500
  }
@@ -444,12 +517,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
444
517
  }
445
518
  if (!changed) return { file, changed: false };
446
519
  if (!Object.keys(hooks).length) delete settings2.hooks;
447
- if (opts.created && !Object.keys(settings2).length) {
448
- unlinkSync(file);
449
- return { file, changed: true, note: "removed (sessionpipe created it)" };
450
- }
451
- writeFileSync(file, formatJson(settings2, f));
452
- return { file, changed: true };
520
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
521
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
453
522
  }
454
523
  function installedClaudeShaped(file, harness, events, cmd, o) {
455
524
  const f = readJsonFile(file);
@@ -528,7 +597,7 @@ var claudeCode = {
528
597
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
529
598
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
530
599
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
531
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
600
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
532
601
  return line ? [hooks, line] : [hooks];
533
602
  }),
534
603
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -536,7 +605,9 @@ var claudeCode = {
536
605
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
537
606
  created: opts.created?.includes(settingsIn(d))
538
607
  });
539
- return line && !hooks.changed ? line : hooks;
608
+ if (line?.changed && !line.skipped && hooks.skipped)
609
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
610
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
540
611
  }),
541
612
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
542
613
  file: settingsIn(d),
@@ -631,7 +702,8 @@ var claudeCode = {
631
702
  attention_id: call_id ?? permissionId(),
632
703
  kind: "permission",
633
704
  tool,
634
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
705
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
706
+ agent_id
635
707
  })
636
708
  );
637
709
  break;
@@ -640,7 +712,8 @@ var claudeCode = {
640
712
  ev("attention.needed", {
641
713
  attention_id: `notif-${Date.now()}`,
642
714
  kind: notifKind(s.notification_type),
643
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
715
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
716
+ agent_id
644
717
  })
645
718
  );
646
719
  break;
@@ -765,8 +838,11 @@ function statusLine(file, want, cmd) {
765
838
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
766
839
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
767
840
  else delete f.value.statusLine;
768
- writeFileSync2(file, formatJson(f.value, f));
769
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
841
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
842
+ file,
843
+ changed: true,
844
+ note: want ? "status line written (account limits)" : "status line removed"
845
+ };
770
846
  }
771
847
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
772
848
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1635,6 +1711,41 @@ import path7 from "node:path";
1635
1711
  import { spawn } from "node:child_process";
1636
1712
  import { accessSync, constants, readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
1637
1713
  import path6 from "node:path";
1714
+
1715
+ // src/launched-by.ts
1716
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
1717
+ var DELEGATED_BY_ENV = "SESSIONPIPE_ASKED_BY";
1718
+ var ASK_CHAIN_ENV = "SESSIONPIPE_ASK_CHAIN";
1719
+ var LINEAGE_ENV = [LAUNCHED_BY_ENV, DELEGATED_BY_ENV, ASK_CHAIN_ENV];
1720
+ var SESSION_REF = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
1721
+ function withoutLineage(env) {
1722
+ const e = { ...env };
1723
+ for (const k of LINEAGE_ENV) delete e[k];
1724
+ return e;
1725
+ }
1726
+ function refIn(env, name) {
1727
+ const v = env[name]?.trim();
1728
+ return v && SESSION_REF.test(v) ? v : void 0;
1729
+ }
1730
+ function launchedBy(env) {
1731
+ return refIn(env, LAUNCHED_BY_ENV);
1732
+ }
1733
+ function delegatedBy(env) {
1734
+ return refIn(env, DELEGATED_BY_ENV);
1735
+ }
1736
+ function readHook(adapter, input) {
1737
+ const r = adapter.fromHook(input);
1738
+ if (r && !r.session.parent_id) {
1739
+ const self = `${adapter.name}:${r.session.id}`;
1740
+ const launcher = launchedBy(input.env);
1741
+ if (launcher && launcher !== self) r.session.launched_by = launcher;
1742
+ const asker = delegatedBy(input.env);
1743
+ if (asker && asker !== self) r.session.delegated_by = asker;
1744
+ }
1745
+ return r;
1746
+ }
1747
+
1748
+ // src/adapters/headless.ts
1638
1749
  var JOB_TIMEOUT_MS = 30 * 6e4;
1639
1750
  var OUT_CAP = 1024 * 1024;
1640
1751
  function isExe(p) {
@@ -1682,7 +1793,7 @@ function probe(bin, args) {
1682
1793
  return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1683
1794
  }
1684
1795
  function jobEnv(env = process.env) {
1685
- return { ...env, SESSIONPIPE_CONTROL_JOB: "1" };
1796
+ return { ...withoutLineage(env), SESSIONPIPE_CONTROL_JOB: "1" };
1686
1797
  }
1687
1798
  function runHeadless(bin, args, o) {
1688
1799
  return new Promise((resolve) => {
@@ -1904,7 +2015,9 @@ function startAcp(o) {
1904
2015
  try {
1905
2016
  child = spawn2(c.file, c.args, {
1906
2017
  cwd: o.cwd,
1907
- env: jobEnv(o.env),
2018
+ // The caller's env as given (the daemon's is jobEnv's already, with the ask's
2019
+ // lineage on top), marked as a control job.
2020
+ env: { ...o.env, SESSIONPIPE_CONTROL_JOB: "1" },
1908
2021
  stdio: ["pipe", "pipe", "pipe"],
1909
2022
  detached: process.platform !== "win32",
1910
2023
  windowsHide: true
@@ -2091,18 +2204,18 @@ function startAcp(o) {
2091
2204
  );
2092
2205
  const stop = str(res?.stopReason);
2093
2206
  const words = t.text.trim() || `(${label} finished without a written reply.)`;
2094
- const refused = deniedNote(t.denied, t.count);
2095
- if (stop === "end_turn" || stop === "max_tokens") return { reply: words + refused };
2207
+ const refused2 = deniedNote(t.denied, t.count);
2208
+ if (stop === "end_turn" || stop === "max_tokens") return { reply: words + refused2 };
2096
2209
  if (stop === "refusal") return { reply: `${words}
2097
2210
 
2098
- (${label} refused the request.)${refused}` };
2211
+ (${label} refused the request.)${refused2}` };
2099
2212
  if (stop === "cancelled")
2100
2213
  return timedOut ? {
2101
2214
  reply: "",
2102
2215
  error: `${label} was still working after ${Math.round(limit / 6e4)} minutes, so it was stopped`
2103
2216
  } : { reply: `${words}
2104
2217
 
2105
- (The turn was cancelled.)${refused}` };
2218
+ (The turn was cancelled.)${refused2}` };
2106
2219
  return { reply: "", error: `${label} ended the turn with ${stop || "no stop reason"}` };
2107
2220
  } catch (e) {
2108
2221
  return { reply: "", error: e.message };
@@ -2163,16 +2276,7 @@ function startAcp(o) {
2163
2276
  }
2164
2277
 
2165
2278
  // src/adapters/antigravity.ts
2166
- import {
2167
- existsSync as existsSync3,
2168
- mkdirSync as mkdirSync2,
2169
- readdirSync as readdirSync3,
2170
- readFileSync as readFileSync5,
2171
- realpathSync,
2172
- statSync as statSync4,
2173
- unlinkSync as unlinkSync2,
2174
- writeFileSync as writeFileSync3
2175
- } from "node:fs";
2279
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync, statSync as statSync4 } from "node:fs";
2176
2280
  import path8 from "node:path";
2177
2281
  var NAME2 = "antigravity";
2178
2282
  var HOOK_NAME = "sessionpipe";
@@ -2204,10 +2308,8 @@ var antigravity = {
2204
2308
  const e = entry(cmd);
2205
2309
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
2206
2310
  hooks[HOOK_NAME] = e;
2207
- mkdirSync2(path8.dirname(file), { recursive: true });
2208
- writeFileSync3(file, formatJson(hooks, f));
2209
2311
  return [
2210
- {
2312
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
2211
2313
  file,
2212
2314
  changed: true,
2213
2315
  created: !f.existed,
@@ -2223,12 +2325,8 @@ var antigravity = {
2223
2325
  const hooks = f.value;
2224
2326
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
2225
2327
  delete hooks[HOOK_NAME];
2226
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
2227
- unlinkSync2(file);
2228
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
2229
- }
2230
- writeFileSync3(file, formatJson(hooks, f));
2231
- return [{ file, changed: true }];
2328
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
2329
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
2232
2330
  },
2233
2331
  installed(cmd, env = process.env) {
2234
2332
  const file = hooksFile(env);
@@ -2591,7 +2689,10 @@ var reading = () => new AgyReading();
2591
2689
  // src/adapters/claude-code-control.ts
2592
2690
  var claude_code_control_exports = {};
2593
2691
  __export(claude_code_control_exports, {
2692
+ CLAUDE_MODES: () => CLAUDE_MODES,
2594
2693
  JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
2694
+ MODE_RANK: () => MODE_RANK,
2695
+ NO_HOME_SETTINGS: () => NO_HOME_SETTINGS,
2595
2696
  ceilingLevel: () => ceilingLevel,
2596
2697
  claudeArgs: () => claudeArgs,
2597
2698
  claudeEnv: () => claudeEnv,
@@ -2601,17 +2702,26 @@ __export(claude_code_control_exports, {
2601
2702
  detectCaps: () => detectCaps,
2602
2703
  findClaude: () => findClaude,
2603
2704
  findTranscript: () => findTranscript,
2705
+ findTranscripts: () => findTranscripts,
2706
+ homeSettings: () => homeSettings,
2707
+ hookMode: () => hookMode,
2604
2708
  isUuid: () => isUuid,
2605
2709
  liveProcess: () => liveProcess,
2710
+ liveRecord: () => liveRecord,
2711
+ liveRecords: () => liveRecords,
2606
2712
  loginCommand: () => loginCommand,
2607
2713
  loginHint: () => loginHint,
2608
2714
  modeFlags: () => modeFlags,
2715
+ modeRank: () => modeRank,
2609
2716
  offeredLevels: () => offeredLevels2,
2610
2717
  outputArgs: () => outputArgs,
2611
2718
  parseAuthStatus: () => parseAuthStatus,
2612
2719
  parseCaps: () => parseCaps,
2613
2720
  parseResult: () => parseResult,
2721
+ peerClass: () => peerClass,
2614
2722
  recentlyWritten: () => recentlyWritten,
2723
+ relaunchMode: () => relaunchMode,
2724
+ resolveHome: () => resolveHome,
2615
2725
  runClaude: () => runClaude,
2616
2726
  sessionFolder: () => sessionFolder
2617
2727
  });
@@ -2630,10 +2740,10 @@ var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
2630
2740
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
2631
2741
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
2632
2742
  var FIELDS = {
2633
- prompt: ["text", "files"],
2743
+ prompt: ["text", "files", "reply_to"],
2634
2744
  "permission.answer": ["for", "decision"],
2635
2745
  cancel: [],
2636
- start: ["text", "cwd", "files", "permission", "model"],
2746
+ start: ["text", "cwd", "files", "permission", "model", "reply_to"],
2637
2747
  "key.add": ["key", "name"],
2638
2748
  pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
2639
2749
  };
@@ -2705,6 +2815,8 @@ var LEVEL_MODE = {
2705
2815
  };
2706
2816
  function modeFlags(level, caps) {
2707
2817
  if (level === "inherit") return { args: [] };
2818
+ if (caps.unread)
2819
+ return { error: `this machine couldn't read its Claude Code's options (${caps.unread}); try again in a minute` };
2708
2820
  if (level === "auto" && !caps.modes.includes("auto"))
2709
2821
  return { error: "this machine is in auto mode, but its Claude Code has no auto permission mode" };
2710
2822
  if (level !== "safe") {
@@ -2724,6 +2836,7 @@ function modeFlags(level, caps) {
2724
2836
  }
2725
2837
  var ceilingLevel = (mode) => mode === "inherit" ? "safe" : mode;
2726
2838
  function offeredLevels2(mode, caps) {
2839
+ if (caps.unread) return [];
2727
2840
  return START_PERMISSIONS.filter(
2728
2841
  (l) => levelRank(l) <= levelRank(ceilingLevel(mode)) && (l === "safe" || caps.modes.includes(LEVEL_MODE[l]))
2729
2842
  );
@@ -2749,7 +2862,7 @@ function claudeArgs(o) {
2749
2862
  };
2750
2863
  }
2751
2864
  function claudeEnv(configDir, env = process.env) {
2752
- const e = { ...env };
2865
+ const e = withoutLineage(env);
2753
2866
  for (const k of [
2754
2867
  "CLAUDECODE",
2755
2868
  "CLAUDE_CODE_SESSION_ID",
@@ -2806,43 +2919,73 @@ function loginHint(configDir, platform = process.platform) {
2806
2919
  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.`;
2807
2920
  return `Claude Code (${shown}) isn't signed in on this machine: run ${login} there.`;
2808
2921
  }
2809
- function detectCaps(bin) {
2810
- let help = "";
2922
+ function detectCaps(bin, timeoutMs = 3e4) {
2811
2923
  const c = probe(bin, ["--help"]);
2812
2924
  try {
2813
- help = execFileSync3(c.file, c.args, {
2814
- encoding: "utf8",
2815
- timeout: 3e4,
2816
- env: claudeEnv(),
2817
- stdio: ["ignore", "pipe", "pipe"],
2818
- maxBuffer: 4 * 1024 * 1024,
2819
- shell: c.shell
2820
- });
2925
+ return parseCaps(
2926
+ execFileSync3(c.file, c.args, {
2927
+ encoding: "utf8",
2928
+ timeout: timeoutMs,
2929
+ env: claudeEnv(),
2930
+ stdio: ["ignore", "pipe", "pipe"],
2931
+ maxBuffer: 4 * 1024 * 1024,
2932
+ shell: c.shell
2933
+ })
2934
+ );
2821
2935
  } catch (e) {
2822
- help = String(e?.stdout || "");
2936
+ const err = e;
2937
+ const caps = parseCaps(String(err?.stdout || ""));
2938
+ if (caps.hasMode) return caps;
2939
+ const why = err?.code === "ETIMEDOUT" ? `timed out after ${timeoutMs / 1e3} s` : "failed";
2940
+ return { ...caps, unread: `\`${path10.basename(bin)} --help\` ${why}` };
2823
2941
  }
2824
- return parseCaps(help);
2825
2942
  }
2826
- function findTranscript(session, dirs) {
2827
- if (!UUID_RE2.test(session)) return null;
2828
- for (const dir of dirs) {
2829
- let projects = [];
2943
+ function transcriptIn(session, dir) {
2944
+ let projects = [];
2945
+ try {
2946
+ projects = readdirSync4(path10.join(dir, "projects"), { withFileTypes: true });
2947
+ } catch {
2948
+ return null;
2949
+ }
2950
+ for (const p of projects) {
2951
+ if (!p.isDirectory()) continue;
2952
+ const file = path10.join(dir, "projects", p.name, `${session}.jsonl`);
2830
2953
  try {
2831
- projects = readdirSync4(path10.join(dir, "projects"), { withFileTypes: true });
2954
+ if (statSync5(file).isFile()) return { file, configDir: dir, project: p.name };
2832
2955
  } catch {
2833
- continue;
2834
- }
2835
- for (const p of projects) {
2836
- if (!p.isDirectory()) continue;
2837
- const file = path10.join(dir, "projects", p.name, `${session}.jsonl`);
2838
- try {
2839
- if (statSync5(file).isFile()) return { file, configDir: dir, project: p.name };
2840
- } catch {
2841
- }
2842
2956
  }
2843
2957
  }
2844
2958
  return null;
2845
2959
  }
2960
+ function findTranscript(session, dirs) {
2961
+ if (!UUID_RE2.test(session)) return null;
2962
+ for (const dir of dirs) {
2963
+ const t = transcriptIn(session, dir);
2964
+ if (t) return t;
2965
+ }
2966
+ return null;
2967
+ }
2968
+ function findTranscripts(session, dirs) {
2969
+ if (!UUID_RE2.test(session)) return [];
2970
+ const out = [];
2971
+ for (const dir of dirs) {
2972
+ const t = transcriptIn(session, dir);
2973
+ if (t) out.push(t);
2974
+ }
2975
+ return out;
2976
+ }
2977
+ var homeName = (t) => path10.basename(t.configDir);
2978
+ function resolveHome(session, dirs) {
2979
+ const all = findTranscripts(session, dirs);
2980
+ if (!all.length) return null;
2981
+ if (all.length === 1) return { transcript: all[0] };
2982
+ const live = all.filter((t) => liveRecords(session, t.configDir).length > 0);
2983
+ if (live.length === 1) return { transcript: live[0] };
2984
+ return {
2985
+ ambiguous: [...new Set(all.map(homeName))].sort(),
2986
+ live: [...new Set(live.map(homeName))].sort()
2987
+ };
2988
+ }
2846
2989
  var projectName = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
2847
2990
  function sessionFolder(t, cap2 = 8 * 1024 * 1024) {
2848
2991
  let first = null;
@@ -2914,22 +3057,105 @@ function pidAlive(pid) {
2914
3057
  return e.code === "EPERM";
2915
3058
  }
2916
3059
  }
2917
- function liveProcess(session, configDir) {
2918
- let names = [];
3060
+ var registryRow = (file, configDir, j) => {
3061
+ const pid = Number(j.pid ?? path10.basename(file).slice(0, -5));
3062
+ const str2 = (v) => typeof v === "string" && v ? v : null;
3063
+ let keyFile = null;
2919
3064
  try {
2920
- names = readdirSync4(path10.join(configDir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
3065
+ const want = new RegExp(`^${pid}\\.[0-9a-f]{64}\\.key$`);
3066
+ const hit = readdirSync4(path10.dirname(file)).find((n) => want.test(n));
3067
+ if (hit) keyFile = path10.join(path10.dirname(file), hit);
2921
3068
  } catch {
2922
- return false;
2923
3069
  }
2924
- for (const n of names) {
3070
+ return {
3071
+ configDir,
3072
+ file,
3073
+ pid,
3074
+ socket: str2(j.messagingSocketPath),
3075
+ peerProtocol: typeof j.peerProtocol === "number" ? j.peerProtocol : null,
3076
+ procStart: str2(j.procStart),
3077
+ status: str2(j.status),
3078
+ kind: str2(j.kind),
3079
+ entrypoint: str2(j.entrypoint),
3080
+ keyFile
3081
+ };
3082
+ };
3083
+ function liveRecords(session, dirs) {
3084
+ const out = [];
3085
+ for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
3086
+ let names = [];
2925
3087
  try {
2926
- const j = JSON.parse(readFileSync6(path10.join(configDir, "sessions", n), "utf8"));
2927
- if (String(j.sessionId) !== session) continue;
2928
- if (pidAlive(Number(j.pid ?? n.slice(0, -5)))) return true;
3088
+ names = readdirSync4(path10.join(dir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
2929
3089
  } catch {
3090
+ continue;
3091
+ }
3092
+ for (const n of names) {
3093
+ const file = path10.join(dir, "sessions", n);
3094
+ let j;
3095
+ try {
3096
+ j = JSON.parse(readFileSync6(file, "utf8"));
3097
+ } catch {
3098
+ continue;
3099
+ }
3100
+ if (String(j.sessionId) !== session) continue;
3101
+ const row = registryRow(file, dir, j);
3102
+ if (pidAlive(row.pid)) out.push(row);
2930
3103
  }
2931
3104
  }
2932
- return false;
3105
+ return out;
3106
+ }
3107
+ function liveRecord(session, dirs) {
3108
+ const all = liveRecords(session, dirs);
3109
+ return all.length === 1 ? all[0] : null;
3110
+ }
3111
+ function liveProcess(session, configDir) {
3112
+ return liveRecords(session, configDir).length > 0;
3113
+ }
3114
+ var MODE_RANK = {
3115
+ plan: 0,
3116
+ default: 1,
3117
+ manual: 1,
3118
+ dontAsk: 1,
3119
+ acceptEdits: 2,
3120
+ auto: 3,
3121
+ bypassPermissions: 4
3122
+ };
3123
+ var CLAUDE_MODES = Object.keys(MODE_RANK);
3124
+ var modeRank = (m) => m && m in MODE_RANK ? MODE_RANK[m] : -1;
3125
+ function hookMode(payload) {
3126
+ if (!payload || typeof payload !== "object") return null;
3127
+ const p = payload;
3128
+ const v = typeof p.permission_mode === "string" ? p.permission_mode : p.permissionMode;
3129
+ return typeof v === "string" && CLAUDE_MODES.includes(v) ? v : null;
3130
+ }
3131
+ function peerClass(mode) {
3132
+ if (mode === "bypassPermissions") return "bypass";
3133
+ if (mode === "default" || mode === "manual" || mode === "dontAsk" || mode === "acceptEdits") return "prompting";
3134
+ return null;
3135
+ }
3136
+ var NO_HOME_SETTINGS = { defaultMode: null, disableAutoMode: false, disableBypass: false };
3137
+ function homeSettings(configDir, home = os5.homedir()) {
3138
+ const file = path10.join(configDir ?? path10.join(home, ".claude"), "settings.json");
3139
+ let j;
3140
+ try {
3141
+ j = JSON.parse(readFileSync6(file, "utf8"));
3142
+ } catch {
3143
+ return { ...NO_HOME_SETTINGS };
3144
+ }
3145
+ if (!j || typeof j !== "object" || Array.isArray(j)) return { ...NO_HOME_SETTINGS };
3146
+ const p = j.permissions && typeof j.permissions === "object" ? j.permissions : {};
3147
+ const off = (k) => j[k] === "disable" || p[k] === "disable";
3148
+ return {
3149
+ defaultMode: typeof p.defaultMode === "string" && CLAUDE_MODES.includes(p.defaultMode) ? p.defaultMode : null,
3150
+ disableAutoMode: off("disableAutoMode"),
3151
+ disableBypass: off("disableBypassPermissionsMode")
3152
+ };
3153
+ }
3154
+ function relaunchMode(recorded, home) {
3155
+ if (recorded === "plan") return recorded;
3156
+ if ((recorded === "dontAsk" || recorded === "acceptEdits") && modeRank(home.defaultMode) >= modeRank(recorded))
3157
+ return recorded;
3158
+ return null;
2933
3159
  }
2934
3160
  function recentlyWritten(file, ourLastEnd, now = Date.now()) {
2935
3161
  let m;
@@ -2971,7 +3197,7 @@ function parseResult(stdout) {
2971
3197
  var isUuid = (s) => typeof s === "string" && UUID_RE2.test(s);
2972
3198
 
2973
3199
  // src/adapters/codex.ts
2974
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
3200
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
2975
3201
  import path11 from "node:path";
2976
3202
 
2977
3203
  // src/adapters/codex-config.ts
@@ -3124,29 +3350,30 @@ var configFile2 = (env) => path11.join(codexHome(env), "config.toml");
3124
3350
  var sessionsDir = (env) => path11.join(codexHome(env), "sessions");
3125
3351
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
3126
3352
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
3353
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
3127
3354
  function installNotify(cmd, env) {
3128
3355
  const file = configFile2(env);
3129
3356
  let text = "";
3357
+ let existed = true;
3130
3358
  try {
3131
3359
  text = readFileSync7(file, "utf8");
3132
- } catch {
3360
+ } catch (e) {
3361
+ existed = e.code !== "ENOENT";
3133
3362
  }
3134
3363
  const argv = cmd([NAME3, "notify"]).argv;
3135
3364
  const line = `notify = ${JSON.stringify(argv)}`;
3136
3365
  const n = readNotify(text);
3137
- const write = (next) => {
3138
- mkdirSync3(path11.dirname(file), { recursive: true });
3139
- writeFileSync4(file, next);
3366
+ const write = (next, note) => {
3367
+ const bad = writeBack(file, next, existed);
3368
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
3140
3369
  };
3141
3370
  if (n && !n.other) {
3142
3371
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
3143
3372
  return { file, changed: false, note: "notify current" };
3144
- write(withNotify(text, line));
3145
- return {
3146
- file,
3147
- changed: true,
3148
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
3149
- };
3373
+ return write(
3374
+ withNotify(text, line),
3375
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
3376
+ );
3150
3377
  }
3151
3378
  if (n?.other) {
3152
3379
  const prev = previousNotify(n.argv);
@@ -3155,20 +3382,20 @@ function installNotify(cmd, env) {
3155
3382
  if (JSON.stringify(prev) === JSON.stringify(argv))
3156
3383
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
3157
3384
  }
3158
- if (!prev || OURS2.test(JSON.stringify(prev))) {
3159
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
3160
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
3161
- }
3385
+ if (!prev || OURS2.test(JSON.stringify(prev)))
3386
+ return write(
3387
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
3388
+ "chained after Codex Computer Use (--previous-notify)"
3389
+ );
3162
3390
  }
3163
3391
  return {
3164
3392
  file,
3165
3393
  changed: false,
3166
3394
  skipped: true,
3167
- 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`
3395
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
3168
3396
  };
3169
3397
  }
3170
- write(withNotify(text, line));
3171
- return { file, changed: true, note: "notify set at the top level" };
3398
+ return write(withNotify(text, line), "notify set at the top level");
3172
3399
  }
3173
3400
  function uninstallNotify(env) {
3174
3401
  const file = configFile2(env);
@@ -3180,15 +3407,13 @@ function uninstallNotify(env) {
3180
3407
  }
3181
3408
  const n = readNotify(text);
3182
3409
  if (!n) return { file, changed: false };
3183
- if (!n.other) {
3184
- writeFileSync4(file, withoutNotify(text));
3185
- return { file, changed: true };
3186
- }
3410
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
3187
3411
  const prev = previousNotify(n.argv);
3188
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
3189
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
3190
- return { file, changed: true };
3191
- }
3412
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
3413
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
3414
+ file,
3415
+ changed: true
3416
+ };
3192
3417
  return { file, changed: false };
3193
3418
  }
3194
3419
  function findRollout(threadId, env = process.env) {
@@ -3220,8 +3445,12 @@ function rolloutMeta(file) {
3220
3445
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
3221
3446
  }
3222
3447
  function ofAnotherThread(file, id) {
3448
+ const other = threadOf(file);
3449
+ return !!other && other !== id.toLowerCase();
3450
+ }
3451
+ function threadOf(file) {
3223
3452
  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);
3224
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
3453
+ return m?.[1] ? m[1].toLowerCase() : null;
3225
3454
  }
3226
3455
  var codex = {
3227
3456
  name: NAME3,
@@ -3300,6 +3529,8 @@ var codex = {
3300
3529
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
3301
3530
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
3302
3531
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
3532
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
3533
+ 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;
3303
3534
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
3304
3535
  const events = [];
3305
3536
  switch (event) {
@@ -3319,17 +3550,30 @@ var codex = {
3319
3550
  );
3320
3551
  break;
3321
3552
  case "PreToolUse":
3322
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
3553
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
3323
3554
  break;
3324
3555
  case "PostToolUse":
3325
3556
  if (tool)
3326
3557
  events.push(
3327
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
3558
+ ev("tool.ended", {
3559
+ tool,
3560
+ call_id,
3561
+ turn_id,
3562
+ agent_id,
3563
+ ok: true,
3564
+ input: s.tool_input,
3565
+ output: s.tool_response
3566
+ })
3328
3567
  );
3329
3568
  break;
3330
3569
  case "PermissionRequest":
3331
3570
  events.push(
3332
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
3571
+ ev("attention.needed", {
3572
+ attention_id: call_id ?? `perm-${Date.now()}`,
3573
+ kind: "permission",
3574
+ tool,
3575
+ agent_id
3576
+ })
3333
3577
  );
3334
3578
  break;
3335
3579
  case "Stop":
@@ -3987,7 +4231,7 @@ var CHAIN_LIMIT = 4e3;
3987
4231
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
3988
4232
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
3989
4233
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
3990
- UNION ALL
4234
+ UNION
3991
4235
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
3992
4236
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
3993
4237
  )
@@ -4265,7 +4509,7 @@ function drivableHarnesses(env = process.env) {
4265
4509
  }
4266
4510
 
4267
4511
  // src/config.ts
4268
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
4512
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync2 } from "node:fs";
4269
4513
  import path15 from "node:path";
4270
4514
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
4271
4515
  function readConfig(file = configFile()) {
@@ -4281,9 +4525,36 @@ function readConfig(file = configFile()) {
4281
4525
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
4282
4526
  }
4283
4527
  }
4528
+ function configProblem(file) {
4529
+ let text;
4530
+ try {
4531
+ text = readFileSync10(file, "utf8");
4532
+ } catch (e) {
4533
+ const code = e.code;
4534
+ return code === "ENOENT" ? void 0 : `could not be read (${code})`;
4535
+ }
4536
+ if (!text.trim()) return void 0;
4537
+ if (text.charCodeAt(0) === 65279) return "starts with a byte-order mark (save it as UTF-8 without one)";
4538
+ try {
4539
+ const j = JSON.parse(text);
4540
+ return j && typeof j === "object" && !Array.isArray(j) ? void 0 : "is not a JSON object";
4541
+ } catch (e) {
4542
+ const m = e.message;
4543
+ const at = (/line \d+ column \d+/.exec(m) ?? /position \d+/.exec(m))?.[0];
4544
+ return `is not valid JSON${at ? ` at ${at}` : ""}`;
4545
+ }
4546
+ }
4547
+ function assertConfigWritable(file = configFile()) {
4548
+ const problem = configProblem(file);
4549
+ if (problem)
4550
+ throw new Error(
4551
+ `${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.`
4552
+ );
4553
+ }
4284
4554
  function writeConfig(c, file = configFile()) {
4285
- mkdirSync4(path15.dirname(file), { recursive: true, mode: 448 });
4286
- writeFileSync5(file, `${JSON.stringify(c, null, 2)}
4555
+ assertConfigWritable(file);
4556
+ mkdirSync2(path15.dirname(file), { recursive: true, mode: 448 });
4557
+ writeFileSync2(file, `${JSON.stringify(c, null, 2)}
4287
4558
  `, { mode: 384 });
4288
4559
  if (process.platform !== "win32" && existsSync8(file)) {
4289
4560
  try {
@@ -4347,36 +4618,21 @@ function makeEvent(a, ctx) {
4347
4618
  };
4348
4619
  }
4349
4620
 
4350
- // src/launched-by.ts
4351
- var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
4352
- var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
4353
- function launchedBy(env) {
4354
- const v = env[LAUNCHED_BY_ENV]?.trim();
4355
- return v && LAUNCHED_BY.test(v) ? v : void 0;
4356
- }
4357
- function readHook(adapter, input) {
4358
- const r = adapter.fromHook(input);
4359
- if (r && !r.session.parent_id) {
4360
- const launcher = launchedBy(input.env);
4361
- if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
4362
- }
4363
- return r;
4364
- }
4365
-
4366
4621
  // src/outbox.ts
4622
+ import { randomBytes as randomBytes3 } from "node:crypto";
4367
4623
  import {
4368
4624
  appendFileSync,
4369
4625
  closeSync as closeSync3,
4370
4626
  existsSync as existsSync9,
4371
- mkdirSync as mkdirSync5,
4627
+ mkdirSync as mkdirSync3,
4372
4628
  openSync as openSync3,
4373
4629
  readdirSync as readdirSync6,
4374
4630
  readFileSync as readFileSync11,
4375
4631
  readSync as readSync3,
4376
4632
  renameSync,
4377
4633
  statSync as statSync6,
4378
- unlinkSync as unlinkSync3,
4379
- writeFileSync as writeFileSync6
4634
+ unlinkSync as unlinkSync2,
4635
+ writeFileSync as writeFileSync3
4380
4636
  } from "node:fs";
4381
4637
  import path16 from "node:path";
4382
4638
  var READ_CHUNK = 1024 * 1024;
@@ -4406,8 +4662,8 @@ var Outbox = class {
4406
4662
  } catch {
4407
4663
  }
4408
4664
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
4409
- mkdirSync5(path16.dirname(f), { recursive: true, mode: 448 });
4410
- writeFileSync6(f, String(n + 1), { mode: 384 });
4665
+ mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
4666
+ writeFileSync3(f, String(n + 1), { mode: 384 });
4411
4667
  return n;
4412
4668
  }
4413
4669
  lastSeqInFile(ref) {
@@ -4425,7 +4681,7 @@ var Outbox = class {
4425
4681
  }
4426
4682
  append(ref, events) {
4427
4683
  if (!events.length) return;
4428
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
4684
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
4429
4685
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
4430
4686
  `, { mode: 384 });
4431
4687
  }
@@ -4495,34 +4751,106 @@ var Outbox = class {
4495
4751
  closeSync3(fd);
4496
4752
  }
4497
4753
  }
4498
- /** Files older than `days` are pruned; the cursor files with them. */
4754
+ /** Files older than `days` are pruned, and each sink's cursor into them. The seq
4755
+ * counter stays: a session resumed after the window counts on from where it was,
4756
+ * so a receiver's (harness, session, seq) dedup doesn't take its new events for
4757
+ * ones it already has, and the sinks read the new file from its start.
4758
+ * It all happens under the drain's `flush` lock, since a drain holds the whole
4759
+ * cursor map and writes it back: a file and its cursor keys go together, so a
4760
+ * session resumed in between can't keep a key past its new file's end. While a
4761
+ * drain runs, nothing is pruned (0); the next prune does it. It never throws: it
4762
+ * is a cleanup, and a hook's drain comes after it. */
4499
4763
  prune(days, now = Date.now()) {
4764
+ let release = null;
4765
+ try {
4766
+ release = takeNamedLock(this.root, "flush");
4767
+ } catch {
4768
+ }
4769
+ if (!release) return 0;
4500
4770
  let n = 0;
4501
- const cutoff = now - days * 864e5;
4502
- for (const ref of this.sessions()) {
4503
- const f = this.file(ref);
4504
- try {
4505
- if (statSync6(f).mtimeMs < cutoff) {
4506
- unlinkSync3(f);
4507
- try {
4508
- unlinkSync3(this.seqFile(ref));
4509
- } catch {
4771
+ try {
4772
+ const cutoff = now - days * 864e5;
4773
+ const aged = this.sessions().filter((ref) => {
4774
+ try {
4775
+ return statSync6(this.file(ref)).mtimeMs < cutoff;
4776
+ } catch {
4777
+ return false;
4778
+ }
4779
+ });
4780
+ this.dropStaleCursors(new Set(aged.map((ref) => `${ref.harness}/${ref.session}`)));
4781
+ for (const ref of aged) {
4782
+ const f = this.file(ref);
4783
+ try {
4784
+ if (statSync6(f).mtimeMs < cutoff) {
4785
+ unlinkSync2(f);
4786
+ n++;
4510
4787
  }
4511
- n++;
4788
+ } catch {
4512
4789
  }
4513
- } catch {
4514
4790
  }
4791
+ } catch {
4792
+ } finally {
4793
+ release();
4515
4794
  }
4516
4795
  return n;
4517
4796
  }
4797
+ /** A sink's cursor into a session file about to be pruned (`gone`), or one already
4798
+ * missing (an earlier prune, an older release, by hand) is removed. The caller
4799
+ * holds the `flush` lock. */
4800
+ dropStaleCursors(gone) {
4801
+ let names = [];
4802
+ try {
4803
+ names = readdirSync6(path16.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
4804
+ } catch {
4805
+ }
4806
+ const stale = (c) => Object.keys(c).filter((k) => {
4807
+ if (gone.has(k)) return true;
4808
+ const i = k.indexOf("/");
4809
+ return !existsSync9(this.file({ harness: k.slice(0, i), session: k.slice(i + 1) }));
4810
+ });
4811
+ for (const n of names) {
4812
+ const s = new Cursors(this.root, n.slice(0, -5));
4813
+ const c = s.read();
4814
+ const keys = stale(c);
4815
+ if (!keys.length) continue;
4816
+ for (const k of keys) delete c[k];
4817
+ s.write(c);
4818
+ }
4819
+ }
4518
4820
  delete(ref) {
4821
+ this.dropCursors(ref);
4519
4822
  for (const f of [this.file(ref), this.seqFile(ref)]) {
4520
4823
  try {
4521
- unlinkSync3(f);
4824
+ unlinkSync2(f);
4522
4825
  } catch {
4523
4826
  }
4524
4827
  }
4525
4828
  }
4829
+ /** The key every sink's cursor holds for a session is removed with the session's
4830
+ * file: the key alone pointed into the old file, and a session that went on (still
4831
+ * running, or resumed later) had its new file's first events unsent until it grew
4832
+ * past that offset. Every file under <state>/cursors is taken (a sink since taken
4833
+ * out of the config still has one; one that doesn't parse reads as empty and is
4834
+ * left), by the key a drain writes (the file's own name, `safeId`) and by the ref
4835
+ * as given. The caller holds the drain's `flush` lock: a drain holds the whole
4836
+ * cursor map and writes it back, so a key dropped under it would come back. */
4837
+ dropCursors(ref) {
4838
+ const keys = /* @__PURE__ */ new Set([`${ref.harness}/${ref.session}`, `${safeId(ref.harness)}/${safeId(ref.session)}`]);
4839
+ let names = [];
4840
+ try {
4841
+ names = readdirSync6(path16.join(this.root, "cursors")).filter((n) => n.endsWith(".json"));
4842
+ } catch {
4843
+ return;
4844
+ }
4845
+ for (const n of names) {
4846
+ const s = new Cursors(this.root, n.slice(0, -5));
4847
+ const c = s.read();
4848
+ const had = Object.keys(c).filter((k) => keys.has(k));
4849
+ if (!had.length) continue;
4850
+ for (const k of had) delete c[k];
4851
+ s.write(c);
4852
+ }
4853
+ }
4526
4854
  };
4527
4855
  var Cursors = class {
4528
4856
  constructor(root, sink) {
@@ -4534,18 +4862,21 @@ var Cursors = class {
4534
4862
  file() {
4535
4863
  return path16.join(this.root, "cursors", `${safeId(this.sink)}.json`);
4536
4864
  }
4865
+ /** The map; empty when the file is missing, cut off mid-write, or holds anything
4866
+ * but an object (`null`, a list), so a drain never indexes into those. */
4537
4867
  read() {
4538
4868
  try {
4539
- return JSON.parse(readFileSync11(this.file(), "utf8"));
4869
+ const c = JSON.parse(readFileSync11(this.file(), "utf8"));
4870
+ return c && typeof c === "object" && !Array.isArray(c) ? c : {};
4540
4871
  } catch {
4541
4872
  return {};
4542
4873
  }
4543
4874
  }
4544
4875
  write(c) {
4545
4876
  const f = this.file();
4546
- mkdirSync5(path16.dirname(f), { recursive: true, mode: 448 });
4877
+ mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
4547
4878
  const tmp = `${f}.${process.pid}.tmp`;
4548
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
4879
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
4549
4880
  renameSync(tmp, f);
4550
4881
  }
4551
4882
  key(ref) {
@@ -4557,25 +4888,54 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
4557
4888
  }
4558
4889
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
4559
4890
  const dir = path16.join(root, "locks");
4560
- mkdirSync5(dir, { recursive: true, mode: 448 });
4891
+ mkdirSync3(dir, { recursive: true, mode: 448 });
4561
4892
  const f = path16.join(dir, `${name}.lock`);
4893
+ const nonce = randomBytes3(8).toString("hex");
4894
+ const mine = `${process.pid} ${nonce}`;
4895
+ let body = null;
4562
4896
  try {
4563
- if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync11(f, "utf8")))) unlinkSync3(f);
4564
- else return null;
4897
+ body = readFileSync11(f, "utf8");
4898
+ const stale = Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(body.split(" ")[0]));
4899
+ if (!stale) return null;
4565
4900
  } catch {
4901
+ body = null;
4902
+ }
4903
+ if (body !== null) {
4904
+ const aside = `${f}.${process.pid}.${nonce}.stale`;
4905
+ try {
4906
+ renameSync(f, aside);
4907
+ } catch {
4908
+ return null;
4909
+ }
4910
+ const moved = readFileSync11(aside, "utf8");
4911
+ unlinkSync2(aside);
4912
+ if (moved !== body) {
4913
+ try {
4914
+ writeFileSync3(f, moved, { flag: "wx" });
4915
+ } catch {
4916
+ }
4917
+ return null;
4918
+ }
4566
4919
  }
4567
4920
  try {
4568
- writeFileSync6(f, String(process.pid), { flag: "wx" });
4921
+ writeFileSync3(f, mine, { flag: "wx" });
4569
4922
  } catch {
4570
4923
  return null;
4571
4924
  }
4572
4925
  return () => {
4573
4926
  try {
4574
- unlinkSync3(f);
4927
+ if (readFileSync11(f, "utf8") === mine) unlinkSync2(f);
4575
4928
  } catch {
4576
4929
  }
4577
4930
  };
4578
4931
  }
4932
+ async function waitNamedLock(root, name, forMs, everyMs = 100) {
4933
+ for (let waited = 0; ; waited += everyMs) {
4934
+ const release = takeNamedLock(root, name);
4935
+ if (release || waited >= forMs) return release;
4936
+ await new Promise((r) => setTimeout(r, everyMs));
4937
+ }
4938
+ }
4579
4939
  function processAlive(pid) {
4580
4940
  if (!Number.isInteger(pid) || pid <= 0) return true;
4581
4941
  try {
@@ -4590,7 +4950,7 @@ function processAlive(pid) {
4590
4950
  import { createHash as createHash4 } from "node:crypto";
4591
4951
  var RULESET_PII = "pii@1";
4592
4952
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
4593
- var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
4953
+ 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;
4594
4954
  function looksPhone(s) {
4595
4955
  const digits = s.replace(/\D/g, "");
4596
4956
  if (digits.length < 7 || digits.length > 15) return false;
@@ -4601,7 +4961,7 @@ function looksPhone(s) {
4601
4961
  function reducePii(text, home) {
4602
4962
  if (!text) return text;
4603
4963
  let out = text.replace(EMAIL, "[email]");
4604
- out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
4964
+ out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
4605
4965
  if (home) {
4606
4966
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4607
4967
  out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -4801,6 +5161,8 @@ var TYPE_TIER = {
4801
5161
  "subagent.ended": 1,
4802
5162
  "context.compacted": 1,
4803
5163
  "limits.observed": 1,
5164
+ "delegation.started": 0,
5165
+ "delegation.ended": 0,
4804
5166
  native: 3
4805
5167
  };
4806
5168
  var DATA_ALLOW = {
@@ -4811,17 +5173,19 @@ var DATA_ALLOW = {
4811
5173
  "session.forgotten": { 0: [] },
4812
5174
  "turn.started": { 0: ["turn_id", "submitted_by", "injected"], 1: ["prompt_chars"] },
4813
5175
  "turn.ended": { 0: ["turn_id", "reason", "ms", "background"], 1: ["error"] },
4814
- "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
4815
- "turn.progress": { 2: ["turn", "user", "assistant", "at"] },
5176
+ "turn.transcript": { 2: ["turn", "user", "assistant", "at", "injected"] },
5177
+ "turn.progress": { 2: ["turn", "user", "assistant", "at", "injected"] },
4816
5178
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
4817
5179
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
4818
5180
  "files.changed": { 1: ["paths"] },
4819
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
5181
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
4820
5182
  "attention.cleared": { 0: ["attention_id", "how"] },
4821
5183
  "subagent.started": { 1: ["agent_id", "agent_type"] },
4822
5184
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
4823
5185
  "context.compacted": { 1: ["trigger"] },
4824
- "limits.observed": { 1: ["windows", "plan", "account_id"] }
5186
+ "limits.observed": { 1: ["windows", "plan", "account_id"] },
5187
+ "delegation.started": { 0: ["ask_id", "via", "harness", "to"], 1: ["text"] },
5188
+ "delegation.ended": { 0: ["ask_id", "to", "ok", "ms", "error", "relayed"], 2: ["reply"] }
4825
5189
  };
4826
5190
  var SESSION_ALLOW = [
4827
5191
  "id",
@@ -4837,7 +5201,8 @@ var SESSION_ALLOW = [
4837
5201
  "account_id",
4838
5202
  "attended",
4839
5203
  "parent_id",
4840
- "launched_by"
5204
+ "launched_by",
5205
+ "delegated_by"
4841
5206
  ];
4842
5207
  var MAX_SIDE = 2e4;
4843
5208
  var MAX_IO = 64 * 1024;
@@ -4876,6 +5241,8 @@ function filterEvent(event, tier, pii, opts = {}) {
4876
5241
  if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
4877
5242
  if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
4878
5243
  if (typeof data.message === "string") data.message = cap(data.message, 200);
5244
+ if (event.type === "delegation.started" && typeof data.text === "string") data.text = cap(data.text, 200);
5245
+ if (event.type === "delegation.ended" && typeof data.reply === "string") data.reply = cap(data.reply, MAX_SIDE);
4879
5246
  const rulesets = [];
4880
5247
  let out = { ...event, tier, session, data, privacy: { rulesets, pii } };
4881
5248
  if (tier > 0) {
@@ -4902,7 +5269,7 @@ function shortId(harness, id) {
4902
5269
  }
4903
5270
 
4904
5271
  // src/sinks/file.ts
4905
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
5272
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
4906
5273
  import path17 from "node:path";
4907
5274
  var FileSink = class {
4908
5275
  constructor(name, file, tier, pii) {
@@ -4917,7 +5284,7 @@ var FileSink = class {
4917
5284
  pii;
4918
5285
  async send(events) {
4919
5286
  try {
4920
- mkdirSync6(path17.dirname(this.file), { recursive: true });
5287
+ mkdirSync4(path17.dirname(this.file), { recursive: true });
4921
5288
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
4922
5289
  `);
4923
5290
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -4955,13 +5322,16 @@ var PROTOCOL_VERSION = 1;
4955
5322
  export {
4956
5323
  ADAPTERS,
4957
5324
  AG_STALE_PRETOOL,
5325
+ ASK_CHAIN_ENV,
4958
5326
  Cursors,
4959
5327
  DEFAULT_CONFIG,
5328
+ DELEGATED_BY_ENV,
4960
5329
  DEVIN_EVENTS,
4961
5330
  FileSink,
4962
5331
  HELLO_HARNESSES_MAX,
4963
5332
  JOB_TIMEOUT_MS,
4964
5333
  LAUNCHED_BY_ENV,
5334
+ LINEAGE_ENV,
4965
5335
  Outbox,
4966
5336
  PROTOCOL_VERSION,
4967
5337
  REDACTED,
@@ -4974,6 +5344,7 @@ export {
4974
5344
  antigravity,
4975
5345
  antigravityAnswer,
4976
5346
  antigravity_control_exports as antigravityControl,
5347
+ assertConfigWritable,
4977
5348
  capTail,
4978
5349
  claudeAccounts,
4979
5350
  claudeCode,
@@ -4983,6 +5354,7 @@ export {
4983
5354
  codex,
4984
5355
  codex_control_exports as codexControl,
4985
5356
  configFile,
5357
+ delegatedBy,
4986
5358
  deniedNote,
4987
5359
  devin,
4988
5360
  devinConfigDir,
@@ -5003,6 +5375,7 @@ export {
5003
5375
  makeEvent,
5004
5376
  notClaudeCode,
5005
5377
  pairTurns,
5378
+ peerAsk,
5006
5379
  processAlive,
5007
5380
  readAntigravity,
5008
5381
  readClaude,
@@ -5025,6 +5398,8 @@ export {
5025
5398
  tilde,
5026
5399
  ulid,
5027
5400
  untilde,
5401
+ waitNamedLock,
5402
+ withoutLineage,
5028
5403
  writeConfig
5029
5404
  };
5030
5405
  //# sourceMappingURL=index.js.map