sessionpipe-core 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe-core 0.13.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
 
@@ -417,6 +417,41 @@ function readJson(file) {
417
417
  const f = readJsonFile(file);
418
418
  return f.ok ? f.value : {};
419
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
+ }
420
455
  function entryFor(event, harness, cmd, o) {
421
456
  const matcher = o.matcherFor(event);
422
457
  return {
@@ -458,8 +493,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
458
493
  }
459
494
  if (changed) {
460
495
  settings2.hooks = hooks;
461
- mkdirSync(path3.dirname(file), { recursive: true });
462
- writeFileSync(file, formatJson(settings2, f));
496
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
497
+ if (bad) return bad;
463
498
  }
464
499
  return { file, changed, created: !f.existed && changed };
465
500
  }
@@ -482,12 +517,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
482
517
  }
483
518
  if (!changed) return { file, changed: false };
484
519
  if (!Object.keys(hooks).length) delete settings2.hooks;
485
- if (opts.created && !Object.keys(settings2).length) {
486
- unlinkSync(file);
487
- return { file, changed: true, note: "removed (sessionpipe created it)" };
488
- }
489
- writeFileSync(file, formatJson(settings2, f));
490
- 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 };
491
522
  }
492
523
  function installedClaudeShaped(file, harness, events, cmd, o) {
493
524
  const f = readJsonFile(file);
@@ -566,7 +597,7 @@ var claudeCode = {
566
597
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
567
598
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
568
599
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
569
- 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);
570
601
  return line ? [hooks, line] : [hooks];
571
602
  }),
572
603
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -574,7 +605,9 @@ var claudeCode = {
574
605
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
575
606
  created: opts.created?.includes(settingsIn(d))
576
607
  });
577
- 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;
578
611
  }),
579
612
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
580
613
  file: settingsIn(d),
@@ -669,7 +702,8 @@ var claudeCode = {
669
702
  attention_id: call_id ?? permissionId(),
670
703
  kind: "permission",
671
704
  tool,
672
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
705
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
706
+ agent_id
673
707
  })
674
708
  );
675
709
  break;
@@ -678,7 +712,8 @@ var claudeCode = {
678
712
  ev("attention.needed", {
679
713
  attention_id: `notif-${Date.now()}`,
680
714
  kind: notifKind(s.notification_type),
681
- 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
682
717
  })
683
718
  );
684
719
  break;
@@ -803,8 +838,11 @@ function statusLine(file, want, cmd) {
803
838
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
804
839
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
805
840
  else delete f.value.statusLine;
806
- writeFileSync2(file, formatJson(f.value, f));
807
- 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
+ };
808
846
  }
809
847
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
810
848
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -2166,18 +2204,18 @@ function startAcp(o) {
2166
2204
  );
2167
2205
  const stop = str(res?.stopReason);
2168
2206
  const words = t.text.trim() || `(${label} finished without a written reply.)`;
2169
- const refused = deniedNote(t.denied, t.count);
2170
- 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 };
2171
2209
  if (stop === "refusal") return { reply: `${words}
2172
2210
 
2173
- (${label} refused the request.)${refused}` };
2211
+ (${label} refused the request.)${refused2}` };
2174
2212
  if (stop === "cancelled")
2175
2213
  return timedOut ? {
2176
2214
  reply: "",
2177
2215
  error: `${label} was still working after ${Math.round(limit / 6e4)} minutes, so it was stopped`
2178
2216
  } : { reply: `${words}
2179
2217
 
2180
- (The turn was cancelled.)${refused}` };
2218
+ (The turn was cancelled.)${refused2}` };
2181
2219
  return { reply: "", error: `${label} ended the turn with ${stop || "no stop reason"}` };
2182
2220
  } catch (e) {
2183
2221
  return { reply: "", error: e.message };
@@ -2238,16 +2276,7 @@ function startAcp(o) {
2238
2276
  }
2239
2277
 
2240
2278
  // src/adapters/antigravity.ts
2241
- import {
2242
- existsSync as existsSync3,
2243
- mkdirSync as mkdirSync2,
2244
- readdirSync as readdirSync3,
2245
- readFileSync as readFileSync5,
2246
- realpathSync,
2247
- statSync as statSync4,
2248
- unlinkSync as unlinkSync2,
2249
- writeFileSync as writeFileSync3
2250
- } from "node:fs";
2279
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync, statSync as statSync4 } from "node:fs";
2251
2280
  import path8 from "node:path";
2252
2281
  var NAME2 = "antigravity";
2253
2282
  var HOOK_NAME = "sessionpipe";
@@ -2279,10 +2308,8 @@ var antigravity = {
2279
2308
  const e = entry(cmd);
2280
2309
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
2281
2310
  hooks[HOOK_NAME] = e;
2282
- mkdirSync2(path8.dirname(file), { recursive: true });
2283
- writeFileSync3(file, formatJson(hooks, f));
2284
2311
  return [
2285
- {
2312
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
2286
2313
  file,
2287
2314
  changed: true,
2288
2315
  created: !f.existed,
@@ -2298,12 +2325,8 @@ var antigravity = {
2298
2325
  const hooks = f.value;
2299
2326
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
2300
2327
  delete hooks[HOOK_NAME];
2301
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
2302
- unlinkSync2(file);
2303
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
2304
- }
2305
- writeFileSync3(file, formatJson(hooks, f));
2306
- 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 }];
2307
2330
  },
2308
2331
  installed(cmd, env = process.env) {
2309
2332
  const file = hooksFile(env);
@@ -3174,7 +3197,7 @@ function parseResult(stdout) {
3174
3197
  var isUuid = (s) => typeof s === "string" && UUID_RE2.test(s);
3175
3198
 
3176
3199
  // src/adapters/codex.ts
3177
- 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";
3178
3201
  import path11 from "node:path";
3179
3202
 
3180
3203
  // src/adapters/codex-config.ts
@@ -3327,29 +3350,30 @@ var configFile2 = (env) => path11.join(codexHome(env), "config.toml");
3327
3350
  var sessionsDir = (env) => path11.join(codexHome(env), "sessions");
3328
3351
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
3329
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";
3330
3354
  function installNotify(cmd, env) {
3331
3355
  const file = configFile2(env);
3332
3356
  let text = "";
3357
+ let existed = true;
3333
3358
  try {
3334
3359
  text = readFileSync7(file, "utf8");
3335
- } catch {
3360
+ } catch (e) {
3361
+ existed = e.code !== "ENOENT";
3336
3362
  }
3337
3363
  const argv = cmd([NAME3, "notify"]).argv;
3338
3364
  const line = `notify = ${JSON.stringify(argv)}`;
3339
3365
  const n = readNotify(text);
3340
- const write = (next) => {
3341
- mkdirSync3(path11.dirname(file), { recursive: true });
3342
- 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 };
3343
3369
  };
3344
3370
  if (n && !n.other) {
3345
3371
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
3346
3372
  return { file, changed: false, note: "notify current" };
3347
- write(withNotify(text, line));
3348
- return {
3349
- file,
3350
- changed: true,
3351
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
3352
- };
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
+ );
3353
3377
  }
3354
3378
  if (n?.other) {
3355
3379
  const prev = previousNotify(n.argv);
@@ -3358,20 +3382,20 @@ function installNotify(cmd, env) {
3358
3382
  if (JSON.stringify(prev) === JSON.stringify(argv))
3359
3383
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
3360
3384
  }
3361
- if (!prev || OURS2.test(JSON.stringify(prev))) {
3362
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
3363
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
3364
- }
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
+ );
3365
3390
  }
3366
3391
  return {
3367
3392
  file,
3368
3393
  changed: false,
3369
3394
  skipped: true,
3370
- 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}`
3371
3396
  };
3372
3397
  }
3373
- write(withNotify(text, line));
3374
- return { file, changed: true, note: "notify set at the top level" };
3398
+ return write(withNotify(text, line), "notify set at the top level");
3375
3399
  }
3376
3400
  function uninstallNotify(env) {
3377
3401
  const file = configFile2(env);
@@ -3383,15 +3407,13 @@ function uninstallNotify(env) {
3383
3407
  }
3384
3408
  const n = readNotify(text);
3385
3409
  if (!n) return { file, changed: false };
3386
- if (!n.other) {
3387
- writeFileSync4(file, withoutNotify(text));
3388
- return { file, changed: true };
3389
- }
3410
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
3390
3411
  const prev = previousNotify(n.argv);
3391
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
3392
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
3393
- return { file, changed: true };
3394
- }
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
+ };
3395
3417
  return { file, changed: false };
3396
3418
  }
3397
3419
  function findRollout(threadId, env = process.env) {
@@ -3423,8 +3445,12 @@ function rolloutMeta(file) {
3423
3445
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
3424
3446
  }
3425
3447
  function ofAnotherThread(file, id) {
3448
+ const other = threadOf(file);
3449
+ return !!other && other !== id.toLowerCase();
3450
+ }
3451
+ function threadOf(file) {
3426
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);
3427
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
3453
+ return m?.[1] ? m[1].toLowerCase() : null;
3428
3454
  }
3429
3455
  var codex = {
3430
3456
  name: NAME3,
@@ -3503,6 +3529,8 @@ var codex = {
3503
3529
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
3504
3530
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
3505
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;
3506
3534
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
3507
3535
  const events = [];
3508
3536
  switch (event) {
@@ -3522,17 +3550,30 @@ var codex = {
3522
3550
  );
3523
3551
  break;
3524
3552
  case "PreToolUse":
3525
- 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 }));
3526
3554
  break;
3527
3555
  case "PostToolUse":
3528
3556
  if (tool)
3529
3557
  events.push(
3530
- 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
+ })
3531
3567
  );
3532
3568
  break;
3533
3569
  case "PermissionRequest":
3534
3570
  events.push(
3535
- 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
+ })
3536
3577
  );
3537
3578
  break;
3538
3579
  case "Stop":
@@ -4190,7 +4231,7 @@ var CHAIN_LIMIT = 4e3;
4190
4231
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
4191
4232
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
4192
4233
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
4193
- UNION ALL
4234
+ UNION
4194
4235
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
4195
4236
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
4196
4237
  )
@@ -4468,7 +4509,7 @@ function drivableHarnesses(env = process.env) {
4468
4509
  }
4469
4510
 
4470
4511
  // src/config.ts
4471
- 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";
4472
4513
  import path15 from "node:path";
4473
4514
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
4474
4515
  function readConfig(file = configFile()) {
@@ -4484,9 +4525,36 @@ function readConfig(file = configFile()) {
4484
4525
  return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
4485
4526
  }
4486
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
+ }
4487
4554
  function writeConfig(c, file = configFile()) {
4488
- mkdirSync4(path15.dirname(file), { recursive: true, mode: 448 });
4489
- 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)}
4490
4558
  `, { mode: 384 });
4491
4559
  if (process.platform !== "win32" && existsSync8(file)) {
4492
4560
  try {
@@ -4551,19 +4619,20 @@ function makeEvent(a, ctx) {
4551
4619
  }
4552
4620
 
4553
4621
  // src/outbox.ts
4622
+ import { randomBytes as randomBytes3 } from "node:crypto";
4554
4623
  import {
4555
4624
  appendFileSync,
4556
4625
  closeSync as closeSync3,
4557
4626
  existsSync as existsSync9,
4558
- mkdirSync as mkdirSync5,
4627
+ mkdirSync as mkdirSync3,
4559
4628
  openSync as openSync3,
4560
4629
  readdirSync as readdirSync6,
4561
4630
  readFileSync as readFileSync11,
4562
4631
  readSync as readSync3,
4563
4632
  renameSync,
4564
4633
  statSync as statSync6,
4565
- unlinkSync as unlinkSync3,
4566
- writeFileSync as writeFileSync6
4634
+ unlinkSync as unlinkSync2,
4635
+ writeFileSync as writeFileSync3
4567
4636
  } from "node:fs";
4568
4637
  import path16 from "node:path";
4569
4638
  var READ_CHUNK = 1024 * 1024;
@@ -4593,8 +4662,8 @@ var Outbox = class {
4593
4662
  } catch {
4594
4663
  }
4595
4664
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
4596
- mkdirSync5(path16.dirname(f), { recursive: true, mode: 448 });
4597
- writeFileSync6(f, String(n + 1), { mode: 384 });
4665
+ mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
4666
+ writeFileSync3(f, String(n + 1), { mode: 384 });
4598
4667
  return n;
4599
4668
  }
4600
4669
  lastSeqInFile(ref) {
@@ -4612,7 +4681,7 @@ var Outbox = class {
4612
4681
  }
4613
4682
  append(ref, events) {
4614
4683
  if (!events.length) return;
4615
- mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
4684
+ mkdirSync3(this.dir(ref.harness), { recursive: true, mode: 448 });
4616
4685
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
4617
4686
  `, { mode: 384 });
4618
4687
  }
@@ -4682,34 +4751,106 @@ var Outbox = class {
4682
4751
  closeSync3(fd);
4683
4752
  }
4684
4753
  }
4685
- /** 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. */
4686
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;
4687
4770
  let n = 0;
4688
- const cutoff = now - days * 864e5;
4689
- for (const ref of this.sessions()) {
4690
- const f = this.file(ref);
4691
- try {
4692
- if (statSync6(f).mtimeMs < cutoff) {
4693
- unlinkSync3(f);
4694
- try {
4695
- unlinkSync3(this.seqFile(ref));
4696
- } 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++;
4697
4787
  }
4698
- n++;
4788
+ } catch {
4699
4789
  }
4700
- } catch {
4701
4790
  }
4791
+ } catch {
4792
+ } finally {
4793
+ release();
4702
4794
  }
4703
4795
  return n;
4704
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
+ }
4705
4820
  delete(ref) {
4821
+ this.dropCursors(ref);
4706
4822
  for (const f of [this.file(ref), this.seqFile(ref)]) {
4707
4823
  try {
4708
- unlinkSync3(f);
4824
+ unlinkSync2(f);
4709
4825
  } catch {
4710
4826
  }
4711
4827
  }
4712
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
+ }
4713
4854
  };
4714
4855
  var Cursors = class {
4715
4856
  constructor(root, sink) {
@@ -4721,18 +4862,21 @@ var Cursors = class {
4721
4862
  file() {
4722
4863
  return path16.join(this.root, "cursors", `${safeId(this.sink)}.json`);
4723
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. */
4724
4867
  read() {
4725
4868
  try {
4726
- 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 : {};
4727
4871
  } catch {
4728
4872
  return {};
4729
4873
  }
4730
4874
  }
4731
4875
  write(c) {
4732
4876
  const f = this.file();
4733
- mkdirSync5(path16.dirname(f), { recursive: true, mode: 448 });
4877
+ mkdirSync3(path16.dirname(f), { recursive: true, mode: 448 });
4734
4878
  const tmp = `${f}.${process.pid}.tmp`;
4735
- writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
4879
+ writeFileSync3(tmp, JSON.stringify(c), { mode: 384 });
4736
4880
  renameSync(tmp, f);
4737
4881
  }
4738
4882
  key(ref) {
@@ -4744,25 +4888,54 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
4744
4888
  }
4745
4889
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
4746
4890
  const dir = path16.join(root, "locks");
4747
- mkdirSync5(dir, { recursive: true, mode: 448 });
4891
+ mkdirSync3(dir, { recursive: true, mode: 448 });
4748
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;
4749
4896
  try {
4750
- if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync11(f, "utf8")))) unlinkSync3(f);
4751
- 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;
4752
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
+ }
4753
4919
  }
4754
4920
  try {
4755
- writeFileSync6(f, String(process.pid), { flag: "wx" });
4921
+ writeFileSync3(f, mine, { flag: "wx" });
4756
4922
  } catch {
4757
4923
  return null;
4758
4924
  }
4759
4925
  return () => {
4760
4926
  try {
4761
- unlinkSync3(f);
4927
+ if (readFileSync11(f, "utf8") === mine) unlinkSync2(f);
4762
4928
  } catch {
4763
4929
  }
4764
4930
  };
4765
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
+ }
4766
4939
  function processAlive(pid) {
4767
4940
  if (!Number.isInteger(pid) || pid <= 0) return true;
4768
4941
  try {
@@ -4777,7 +4950,7 @@ function processAlive(pid) {
4777
4950
  import { createHash as createHash4 } from "node:crypto";
4778
4951
  var RULESET_PII = "pii@1";
4779
4952
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
4780
- 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;
4781
4954
  function looksPhone(s) {
4782
4955
  const digits = s.replace(/\D/g, "");
4783
4956
  if (digits.length < 7 || digits.length > 15) return false;
@@ -4788,7 +4961,7 @@ function looksPhone(s) {
4788
4961
  function reducePii(text, home) {
4789
4962
  if (!text) return text;
4790
4963
  let out = text.replace(EMAIL, "[email]");
4791
- out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
4964
+ out = out.replace(PHONE, (m, date) => !date && looksPhone(m) ? "[phone]" : m);
4792
4965
  if (home) {
4793
4966
  const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4794
4967
  out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
@@ -5005,7 +5178,7 @@ var DATA_ALLOW = {
5005
5178
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
5006
5179
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
5007
5180
  "files.changed": { 1: ["paths"] },
5008
- "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
5181
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message", "agent_id"] },
5009
5182
  "attention.cleared": { 0: ["attention_id", "how"] },
5010
5183
  "subagent.started": { 1: ["agent_id", "agent_type"] },
5011
5184
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
@@ -5096,7 +5269,7 @@ function shortId(harness, id) {
5096
5269
  }
5097
5270
 
5098
5271
  // src/sinks/file.ts
5099
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
5272
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
5100
5273
  import path17 from "node:path";
5101
5274
  var FileSink = class {
5102
5275
  constructor(name, file, tier, pii) {
@@ -5111,7 +5284,7 @@ var FileSink = class {
5111
5284
  pii;
5112
5285
  async send(events) {
5113
5286
  try {
5114
- mkdirSync6(path17.dirname(this.file), { recursive: true });
5287
+ mkdirSync4(path17.dirname(this.file), { recursive: true });
5115
5288
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
5116
5289
  `);
5117
5290
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -5171,6 +5344,7 @@ export {
5171
5344
  antigravity,
5172
5345
  antigravityAnswer,
5173
5346
  antigravity_control_exports as antigravityControl,
5347
+ assertConfigWritable,
5174
5348
  capTail,
5175
5349
  claudeAccounts,
5176
5350
  claudeCode,
@@ -5224,6 +5398,7 @@ export {
5224
5398
  tilde,
5225
5399
  ulid,
5226
5400
  untilde,
5401
+ waitNamedLock,
5227
5402
  withoutLineage,
5228
5403
  writeConfig
5229
5404
  };