sessionpipe-conformance 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.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAeA,gFAAgF;AAChF,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE;IAAE,MAAM,EAAE;QAAE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAanH"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAeA,gFAAgF;AAChF,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE;IAAE,MAAM,EAAE;QAAE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BnH"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe-conformance 0.12.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe-conformance 0.13.1 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/cli.ts
5
5
  import { readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
@@ -16,7 +16,7 @@ var homeDir = () => os.homedir();
16
16
  // ../core/src/adapters/claude-code.ts
17
17
  import { execFileSync as execFileSync2 } from "node:child_process";
18
18
  import { createHash } from "node:crypto";
19
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
19
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
20
20
  import { userInfo } from "node:os";
21
21
  import path3 from "node:path";
22
22
 
@@ -168,7 +168,14 @@ ${m.text}`;
168
168
  continue;
169
169
  }
170
170
  if (cur) turns.push(cur);
171
- cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
171
+ cur = {
172
+ user: m.text,
173
+ assistant: "",
174
+ at: m.at,
175
+ startLine: m.line,
176
+ endLine: m.line,
177
+ ...m.injected ? { injected: true } : {}
178
+ };
172
179
  } else {
173
180
  if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
174
181
  cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
@@ -178,10 +185,17 @@ ${m.text}`;
178
185
  if (cur?.assistant) turns.push(cur);
179
186
  const line = cur && !cur.assistant ? cur.startLine : endLine;
180
187
  const out = {
181
- turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
188
+ turns: turns.map((t) => ({
189
+ user: t.user,
190
+ assistant: t.assistant,
191
+ at: t.at,
192
+ endLine: t.endLine,
193
+ ...t.injected ? { injected: true } : {}
194
+ })),
182
195
  line
183
196
  };
184
- if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
197
+ if (cur && !cur.assistant && cur.user)
198
+ out.open = { user: cur.user, at: cur.at, ...cur.injected ? { injected: true } : {} };
185
199
  return out;
186
200
  }
187
201
  var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
@@ -204,27 +218,51 @@ var textParts = (c, kinds) => {
204
218
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
205
219
  ).map((b) => b.text).join("\n");
206
220
  };
221
+ var SPOKEN = /* @__PURE__ */ new Set(["human", "peer"]);
222
+ var PEER_INTRO = /sent a message[^\n]*:$/;
223
+ var PEER_OUTRO = "\n\nThis came from another ";
224
+ var ENVELOPE = /^<cross-session-message\b[^>]*>\n([\s\S]*)\n<\/cross-session-message>$/;
225
+ function peerAsk(origin, text) {
226
+ const body = origin?.body;
227
+ let t = typeof body === "string" && body.trim() ? body : text;
228
+ if (t !== body) {
229
+ const nl = t.indexOf("\n");
230
+ if (nl > 0 && PEER_INTRO.test(t.slice(0, nl))) t = t.slice(nl + 1);
231
+ const outro = t.indexOf(PEER_OUTRO);
232
+ if (outro > 0) t = t.slice(0, outro);
233
+ }
234
+ return (ENVELOPE.exec(t.trim())?.[1] ?? t).trim();
235
+ }
207
236
  var queuedPrompt = (a) => {
208
237
  if (!a || typeof a !== "object") return "";
209
238
  const q = a;
210
239
  if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
211
- if (q.origin?.kind && q.origin.kind !== "human") return "";
212
- return textParts(q.prompt, ["text"]).trim();
240
+ const kind = q.origin?.kind ?? "human";
241
+ if (!SPOKEN.has(kind)) return "";
242
+ const text = textParts(q.prompt, ["text"]).trim();
243
+ return kind === "peer" ? peerAsk(q.origin, text) : text;
213
244
  };
245
+ var channelText = (text) => /^<channel\b[^>]*>([\s\S]*)<\/channel>$/.exec(text)?.[1]?.trim() ?? "";
214
246
  function readClaude(lines, from) {
215
247
  const msgs = [];
216
248
  for (let i = from; i < lines.length; i++) {
217
249
  const j = lines[i] ? parse(lines[i]) : null;
218
250
  if (!j || j.isSidechain) continue;
219
251
  const origin = j.origin;
220
- if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
221
- continue;
252
+ const channel = j.type === "user" && origin?.kind === "channel";
253
+ const peer = origin?.kind === "peer";
254
+ if (j.type === "user" && (j.isCompactSummary || j.repeatsCompactSummary)) continue;
255
+ if (j.type === "user" && !channel && !peer && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
222
256
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
223
257
  const c = j.message?.content;
224
- if (j.type === "user") {
225
- const text = textParts(c, ["text"]).trim();
258
+ if (channel) {
259
+ const text = channelText(textParts(c, ["text"]).trim());
260
+ if (text) msgs.push({ role: "user", text, at, line: i, injected: true });
261
+ } else if (j.type === "user") {
262
+ const raw = textParts(c, ["text"]).trim();
263
+ const text = peer ? peerAsk(origin, raw) : raw;
226
264
  if (!text || isInjected(text)) continue;
227
- msgs.push({ role: "user", text, at, line: i });
265
+ msgs.push({ role: "user", text, at, line: i, ...peer ? { injected: true } : {} });
228
266
  } else if (j.type === "assistant" && Array.isArray(c)) {
229
267
  const text = textParts(c, ["text"]).trim();
230
268
  if (text) msgs.push({ role: "assistant", text, at, line: i });
@@ -336,6 +374,41 @@ function readJson(file) {
336
374
  const f = readJsonFile(file);
337
375
  return f.ok ? f.value : {};
338
376
  }
377
+ function refused(file, e, what, existed) {
378
+ const { code, syscall } = e;
379
+ const why = `can't ${what} it (${code ?? String(e)})`;
380
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
381
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
382
+ return {
383
+ file,
384
+ changed: true,
385
+ skipped: true,
386
+ ...existed ? {} : { created: true },
387
+ note: `${why} part way; check the file before the harness reads it`
388
+ };
389
+ }
390
+ function writeBack(file, text, existed = true) {
391
+ try {
392
+ mkdirSync(path2.dirname(file), { recursive: true });
393
+ writeFileSync(file, text);
394
+ return null;
395
+ } catch (e) {
396
+ return refused(file, e, "write", existed);
397
+ }
398
+ }
399
+ function removeEmptied(file, empty) {
400
+ try {
401
+ unlinkSync(file);
402
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
403
+ } catch (e) {
404
+ const bad = refused(file, e, "remove", true);
405
+ return writeBack(file, empty) ?? {
406
+ file,
407
+ changed: true,
408
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
409
+ };
410
+ }
411
+ }
339
412
  function entryFor(event, harness, cmd, o) {
340
413
  const matcher = o.matcherFor(event);
341
414
  return {
@@ -377,8 +450,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
377
450
  }
378
451
  if (changed) {
379
452
  settings2.hooks = hooks;
380
- mkdirSync(path2.dirname(file), { recursive: true });
381
- writeFileSync(file, formatJson(settings2, f));
453
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
454
+ if (bad) return bad;
382
455
  }
383
456
  return { file, changed, created: !f.existed && changed };
384
457
  }
@@ -401,12 +474,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
401
474
  }
402
475
  if (!changed) return { file, changed: false };
403
476
  if (!Object.keys(hooks).length) delete settings2.hooks;
404
- if (opts.created && !Object.keys(settings2).length) {
405
- unlinkSync(file);
406
- return { file, changed: true, note: "removed (sessionpipe created it)" };
407
- }
408
- writeFileSync(file, formatJson(settings2, f));
409
- return { file, changed: true };
477
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
478
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
410
479
  }
411
480
  function installedClaudeShaped(file, harness, events, cmd, o) {
412
481
  const f = readJsonFile(file);
@@ -485,7 +554,7 @@ var claudeCode = {
485
554
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
486
555
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
487
556
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
488
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
557
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
489
558
  return line ? [hooks, line] : [hooks];
490
559
  }),
491
560
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -493,7 +562,9 @@ var claudeCode = {
493
562
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
494
563
  created: opts.created?.includes(settingsIn(d))
495
564
  });
496
- return line && !hooks.changed ? line : hooks;
565
+ if (line?.changed && !line.skipped && hooks.skipped)
566
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
567
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
497
568
  }),
498
569
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
499
570
  file: settingsIn(d),
@@ -588,7 +659,8 @@ var claudeCode = {
588
659
  attention_id: call_id ?? permissionId(),
589
660
  kind: "permission",
590
661
  tool,
591
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
662
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
663
+ agent_id
592
664
  })
593
665
  );
594
666
  break;
@@ -597,7 +669,8 @@ var claudeCode = {
597
669
  ev("attention.needed", {
598
670
  attention_id: `notif-${Date.now()}`,
599
671
  kind: notifKind(s.notification_type),
600
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
672
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
673
+ agent_id
601
674
  })
602
675
  );
603
676
  break;
@@ -722,8 +795,11 @@ function statusLine(file, want, cmd) {
722
795
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
723
796
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
724
797
  else delete f.value.statusLine;
725
- writeFileSync2(file, formatJson(f.value, f));
726
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
798
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
799
+ file,
800
+ changed: true,
801
+ note: want ? "status line written (account limits)" : "status line removed"
802
+ };
727
803
  }
728
804
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
729
805
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1140,6 +1216,34 @@ async function claudeAiTitle(bridge, token) {
1140
1216
  // ../core/src/adapters/headless.ts
1141
1217
  import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1142
1218
  import path4 from "node:path";
1219
+
1220
+ // ../core/src/launched-by.ts
1221
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
1222
+ var DELEGATED_BY_ENV = "SESSIONPIPE_ASKED_BY";
1223
+ var SESSION_REF = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
1224
+ function refIn(env, name) {
1225
+ const v = env[name]?.trim();
1226
+ return v && SESSION_REF.test(v) ? v : void 0;
1227
+ }
1228
+ function launchedBy(env) {
1229
+ return refIn(env, LAUNCHED_BY_ENV);
1230
+ }
1231
+ function delegatedBy(env) {
1232
+ return refIn(env, DELEGATED_BY_ENV);
1233
+ }
1234
+ function readHook(adapter, input) {
1235
+ const r = adapter.fromHook(input);
1236
+ if (r && !r.session.parent_id) {
1237
+ const self = `${adapter.name}:${r.session.id}`;
1238
+ const launcher = launchedBy(input.env);
1239
+ if (launcher && launcher !== self) r.session.launched_by = launcher;
1240
+ const asker = delegatedBy(input.env);
1241
+ if (asker && asker !== self) r.session.delegated_by = asker;
1242
+ }
1243
+ return r;
1244
+ }
1245
+
1246
+ // ../core/src/adapters/headless.ts
1143
1247
  var JOB_TIMEOUT_MS = 30 * 6e4;
1144
1248
  var OUT_CAP = 1024 * 1024;
1145
1249
  function isExe(p) {
@@ -1187,16 +1291,7 @@ function probe(bin, args) {
1187
1291
  }
1188
1292
 
1189
1293
  // ../core/src/adapters/antigravity.ts
1190
- import {
1191
- existsSync as existsSync3,
1192
- mkdirSync as mkdirSync2,
1193
- readdirSync as readdirSync3,
1194
- readFileSync as readFileSync4,
1195
- realpathSync,
1196
- statSync as statSync4,
1197
- unlinkSync as unlinkSync2,
1198
- writeFileSync as writeFileSync3
1199
- } from "node:fs";
1294
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync4 } from "node:fs";
1200
1295
  import path5 from "node:path";
1201
1296
  var NAME2 = "antigravity";
1202
1297
  var HOOK_NAME = "sessionpipe";
@@ -1228,10 +1323,8 @@ var antigravity = {
1228
1323
  const e = entry(cmd);
1229
1324
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1230
1325
  hooks[HOOK_NAME] = e;
1231
- mkdirSync2(path5.dirname(file), { recursive: true });
1232
- writeFileSync3(file, formatJson(hooks, f));
1233
1326
  return [
1234
- {
1327
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
1235
1328
  file,
1236
1329
  changed: true,
1237
1330
  created: !f.existed,
@@ -1247,12 +1340,8 @@ var antigravity = {
1247
1340
  const hooks = f.value;
1248
1341
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1249
1342
  delete hooks[HOOK_NAME];
1250
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1251
- unlinkSync2(file);
1252
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1253
- }
1254
- writeFileSync3(file, formatJson(hooks, f));
1255
- return [{ file, changed: true }];
1343
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
1344
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
1256
1345
  },
1257
1346
  installed(cmd, env = process.env) {
1258
1347
  const file = hooksFile(env);
@@ -1475,7 +1564,7 @@ function antigravityFacts(file, opts) {
1475
1564
  }
1476
1565
 
1477
1566
  // ../core/src/adapters/codex.ts
1478
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1567
+ import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
1479
1568
  import path6 from "node:path";
1480
1569
 
1481
1570
  // ../core/src/adapters/codex-config.ts
@@ -1628,29 +1717,30 @@ var configFile = (env) => path6.join(codexHome(env), "config.toml");
1628
1717
  var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1629
1718
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1630
1719
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1720
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
1631
1721
  function installNotify(cmd, env) {
1632
1722
  const file = configFile(env);
1633
1723
  let text = "";
1724
+ let existed = true;
1634
1725
  try {
1635
1726
  text = readFileSync5(file, "utf8");
1636
- } catch {
1727
+ } catch (e) {
1728
+ existed = e.code !== "ENOENT";
1637
1729
  }
1638
1730
  const argv = cmd([NAME3, "notify"]).argv;
1639
1731
  const line = `notify = ${JSON.stringify(argv)}`;
1640
1732
  const n = readNotify(text);
1641
- const write = (next) => {
1642
- mkdirSync3(path6.dirname(file), { recursive: true });
1643
- writeFileSync4(file, next);
1733
+ const write = (next, note) => {
1734
+ const bad = writeBack(file, next, existed);
1735
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
1644
1736
  };
1645
1737
  if (n && !n.other) {
1646
1738
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1647
1739
  return { file, changed: false, note: "notify current" };
1648
- write(withNotify(text, line));
1649
- return {
1650
- file,
1651
- changed: true,
1652
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1653
- };
1740
+ return write(
1741
+ withNotify(text, line),
1742
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1743
+ );
1654
1744
  }
1655
1745
  if (n?.other) {
1656
1746
  const prev = previousNotify(n.argv);
@@ -1659,20 +1749,20 @@ function installNotify(cmd, env) {
1659
1749
  if (JSON.stringify(prev) === JSON.stringify(argv))
1660
1750
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1661
1751
  }
1662
- if (!prev || OURS2.test(JSON.stringify(prev))) {
1663
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1664
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1665
- }
1752
+ if (!prev || OURS2.test(JSON.stringify(prev)))
1753
+ return write(
1754
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
1755
+ "chained after Codex Computer Use (--previous-notify)"
1756
+ );
1666
1757
  }
1667
1758
  return {
1668
1759
  file,
1669
1760
  changed: false,
1670
1761
  skipped: true,
1671
- 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`
1762
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
1672
1763
  };
1673
1764
  }
1674
- write(withNotify(text, line));
1675
- return { file, changed: true, note: "notify set at the top level" };
1765
+ return write(withNotify(text, line), "notify set at the top level");
1676
1766
  }
1677
1767
  function uninstallNotify(env) {
1678
1768
  const file = configFile(env);
@@ -1684,15 +1774,13 @@ function uninstallNotify(env) {
1684
1774
  }
1685
1775
  const n = readNotify(text);
1686
1776
  if (!n) return { file, changed: false };
1687
- if (!n.other) {
1688
- writeFileSync4(file, withoutNotify(text));
1689
- return { file, changed: true };
1690
- }
1777
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
1691
1778
  const prev = previousNotify(n.argv);
1692
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1693
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1694
- return { file, changed: true };
1695
- }
1779
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
1780
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
1781
+ file,
1782
+ changed: true
1783
+ };
1696
1784
  return { file, changed: false };
1697
1785
  }
1698
1786
  function findRollout(threadId, env = process.env) {
@@ -1724,8 +1812,12 @@ function rolloutMeta(file) {
1724
1812
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
1725
1813
  }
1726
1814
  function ofAnotherThread(file, id) {
1815
+ const other = threadOf(file);
1816
+ return !!other && other !== id.toLowerCase();
1817
+ }
1818
+ function threadOf(file) {
1727
1819
  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);
1728
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
1820
+ return m?.[1] ? m[1].toLowerCase() : null;
1729
1821
  }
1730
1822
  var codex = {
1731
1823
  name: NAME3,
@@ -1804,6 +1896,8 @@ var codex = {
1804
1896
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1805
1897
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1806
1898
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1899
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
1900
+ 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;
1807
1901
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1808
1902
  const events = [];
1809
1903
  switch (event) {
@@ -1823,17 +1917,30 @@ var codex = {
1823
1917
  );
1824
1918
  break;
1825
1919
  case "PreToolUse":
1826
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1920
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
1827
1921
  break;
1828
1922
  case "PostToolUse":
1829
1923
  if (tool)
1830
1924
  events.push(
1831
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1925
+ ev("tool.ended", {
1926
+ tool,
1927
+ call_id,
1928
+ turn_id,
1929
+ agent_id,
1930
+ ok: true,
1931
+ input: s.tool_input,
1932
+ output: s.tool_response
1933
+ })
1832
1934
  );
1833
1935
  break;
1834
1936
  case "PermissionRequest":
1835
1937
  events.push(
1836
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1938
+ ev("attention.needed", {
1939
+ attention_id: call_id ?? `perm-${Date.now()}`,
1940
+ kind: "permission",
1941
+ tool,
1942
+ agent_id
1943
+ })
1837
1944
  );
1838
1945
  break;
1839
1946
  case "Stop":
@@ -2323,7 +2430,7 @@ var CHAIN_LIMIT = 4e3;
2323
2430
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2324
2431
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2325
2432
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2326
- UNION ALL
2433
+ UNION
2327
2434
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2328
2435
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2329
2436
  )
@@ -2591,22 +2698,6 @@ function prune5(o) {
2591
2698
  var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2592
2699
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2593
2700
 
2594
- // ../core/src/launched-by.ts
2595
- var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2596
- var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2597
- function launchedBy(env) {
2598
- const v = env[LAUNCHED_BY_ENV]?.trim();
2599
- return v && LAUNCHED_BY.test(v) ? v : void 0;
2600
- }
2601
- function readHook(adapter, input) {
2602
- const r = adapter.fromHook(input);
2603
- if (r && !r.session.parent_id) {
2604
- const launcher = launchedBy(input.env);
2605
- if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2606
- }
2607
- return r;
2608
- }
2609
-
2610
2701
  // src/index.ts
2611
2702
  function matches(expect, got, pathStr = "") {
2612
2703
  if (expect === "*") return null;
@@ -2631,7 +2722,18 @@ function matches(expect, got, pathStr = "") {
2631
2722
  async function runAdapterFixtures(files) {
2632
2723
  const results = [];
2633
2724
  for (const file of files) {
2634
- const fx = JSON.parse(readFileSync7(file, "utf8"));
2725
+ let fx;
2726
+ try {
2727
+ fx = JSON.parse(readFileSync7(file, "utf8"));
2728
+ } catch (e) {
2729
+ const code = e.code;
2730
+ results.push({ file, ok: false, why: code ? `not readable: ${code}` : `not JSON: ${e.message}` });
2731
+ continue;
2732
+ }
2733
+ if (typeof fx?.harness !== "string" || !fx.input || !Array.isArray(fx.expect)) {
2734
+ results.push({ file, ok: false, why: "not an adapter fixture: it needs harness, input and expect" });
2735
+ continue;
2736
+ }
2635
2737
  const adapter = adapterByName(fx.harness);
2636
2738
  if (!adapter) {
2637
2739
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
@@ -2673,14 +2775,27 @@ var walk = (d) => readdirSync5(d).flatMap((n) => {
2673
2775
  async function main(argv, io = process) {
2674
2776
  const target = argv[0];
2675
2777
  if (!target) {
2676
- io.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
2778
+ io.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | fixture file | https://receiver>\n");
2677
2779
  return 2;
2678
2780
  }
2679
- if (/^https?:\/\//.test(target)) {
2781
+ if (/^https?:\/\//i.test(target)) {
2680
2782
  io.stdout.write("receiver scenarios land in M3\n");
2681
2783
  return 2;
2682
2784
  }
2683
- const r = await runAdapterFixtures(walk(target));
2785
+ let files;
2786
+ try {
2787
+ files = statSync5(target).isDirectory() ? walk(target) : [target];
2788
+ } catch (e) {
2789
+ io.stdout.write(`sessionpipe-conformance: ${e.message}
2790
+ `);
2791
+ return 2;
2792
+ }
2793
+ if (!files.length) {
2794
+ io.stdout.write(`sessionpipe-conformance: no .json fixtures under ${target}
2795
+ `);
2796
+ return 2;
2797
+ }
2798
+ const r = await runAdapterFixtures(files);
2684
2799
  io.stdout.write(`${r.table}
2685
2800
  ${r.results.length - r.failed}/${r.results.length} passed
2686
2801
  `);