@junghanacs/entwurf 0.13.1 → 0.14.0

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 (152) hide show
  1. package/AGENTS.md +48 -15
  2. package/BASELINE.md +3 -3
  3. package/CHANGELOG.md +45 -0
  4. package/CONTRIBUTING.md +13 -9
  5. package/DELIVERY.md +7 -6
  6. package/README.md +27 -27
  7. package/VERIFY.md +22 -14
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +9 -4
  12. package/docs/external-mcp-host.md +4 -5
  13. package/docs/setup-clean-host.md +8 -7
  14. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +148 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  37. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  38. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  39. package/mcp/entwurf-bridge/src/index.ts +164 -28
  40. package/mcp/entwurf-bridge/start.sh +2 -2
  41. package/mcp/entwurf-bridge/test.sh +23 -9
  42. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  43. package/package.json +22 -11
  44. package/pi-extensions/entwurf-control.ts +218 -40
  45. package/pi-extensions/lib/acp/backend.ts +71 -12
  46. package/pi-extensions/lib/acp/overlay.ts +8 -6
  47. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  48. package/pi-extensions/lib/entwurf-core.ts +15 -15
  49. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  50. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  51. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  52. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  53. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  54. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  55. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  56. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  57. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  58. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  59. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  60. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  61. package/pi-extensions/lib/meta-session.ts +93 -5
  62. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  63. package/pi-extensions/lib/mux-launch.ts +267 -0
  64. package/pi-extensions/lib/mux-placement.ts +387 -0
  65. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  66. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  67. package/pi-extensions/lib/session-id.js +8 -5
  68. package/pi-extensions/lib/socket-discovery.ts +3 -3
  69. package/run.sh +471 -201
  70. package/scripts/agy-bridge-config.py +5 -1
  71. package/scripts/check-acp-backend-preflight.ts +1 -1
  72. package/scripts/check-acp-overlay.ts +13 -3
  73. package/scripts/check-acp-stream-hooks.ts +504 -0
  74. package/scripts/check-elapsed.sh +25 -0
  75. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  76. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  77. package/scripts/check-entwurf-control-rpc.ts +4 -3
  78. package/scripts/check-entwurf-resume-args.ts +72 -70
  79. package/scripts/check-entwurf-session-identity.ts +14 -10
  80. package/scripts/check-entwurf-v2-contract.ts +34 -59
  81. package/scripts/check-entwurf-v2-decider.ts +17 -177
  82. package/scripts/check-entwurf-v2-lock.ts +5 -2
  83. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  84. package/scripts/check-entwurf-v2-production.ts +2 -91
  85. package/scripts/check-entwurf-v2-release.ts +10 -105
  86. package/scripts/check-entwurf-v2-runner.ts +4 -85
  87. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  88. package/scripts/check-entwurf-v2-send.ts +0 -28
  89. package/scripts/check-entwurf-v2-surface.ts +157 -128
  90. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  91. package/scripts/check-fresh-cut-gate.sh +1 -1
  92. package/scripts/check-gate-qualification.ts +98 -7
  93. package/scripts/check-install-container.sh +10 -2
  94. package/scripts/check-install-surface.ts +1 -1
  95. package/scripts/check-keyset-overlap.py +1 -1
  96. package/scripts/check-meta-facts.ts +249 -0
  97. package/scripts/check-meta-identity-consumers.ts +1 -1
  98. package/scripts/check-meta-session.ts +169 -0
  99. package/scripts/check-mux-launch-tmux.ts +316 -0
  100. package/scripts/check-mux-launch.ts +288 -0
  101. package/scripts/check-mux-launcher-fence.ts +264 -0
  102. package/scripts/check-mux-parent-artifact.ts +195 -0
  103. package/scripts/check-mux-placement-tmux.ts +322 -0
  104. package/scripts/check-mux-placement.ts +323 -0
  105. package/scripts/check-mux-resume-call.ts +282 -0
  106. package/scripts/check-probe-cli-shim.ts +25 -22
  107. package/scripts/check-probe-ordering.ts +84 -76
  108. package/scripts/check-release-gate-outcomes.ts +127 -7
  109. package/scripts/check-resume-launch-identity.ts +244 -0
  110. package/scripts/check-socket-discovery.ts +1 -1
  111. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  112. package/scripts/inventory-verification-surface.ts +349 -0
  113. package/scripts/lib/claude-launcher-fence.ts +322 -0
  114. package/scripts/lib/mutation-qualify.ts +109 -3
  115. package/scripts/meta-bridge-doctor.sh +6 -8
  116. package/scripts/meta-facts.ts +60 -0
  117. package/scripts/mutants/acp-overlay.json +17 -0
  118. package/scripts/mutants/acp-stream-hooks.json +158 -0
  119. package/scripts/mutants/bridge-boot-resume.json +45 -0
  120. package/scripts/mutants/meta-facts.json +50 -0
  121. package/scripts/mutants/meta-identity.json +36 -0
  122. package/scripts/mutants/mux-boundary.json +196 -0
  123. package/scripts/mutants/mux-fresh-call.json +185 -0
  124. package/scripts/mutants/mux-launcher-fence.json +123 -0
  125. package/scripts/mutants/mux-parent-artifact.json +39 -0
  126. package/scripts/mutants/mux-resume-call.json +148 -0
  127. package/scripts/mutants/probe-ordering.json +0 -1037
  128. package/scripts/mutants/release-gate.json +35 -0
  129. package/scripts/mutants/resume-args.json +76 -0
  130. package/scripts/mutants/resume-launch-identity.json +96 -0
  131. package/scripts/mutants/v2-surface.json +58 -18
  132. package/scripts/mutants/v2-visible-resume.json +215 -0
  133. package/scripts/new-session-id.ts +9 -4
  134. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  135. package/scripts/smoke-agy-native-push-live.ts +6 -17
  136. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  137. package/scripts/smoke-meta-honesty.sh +1 -1
  138. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  139. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  140. package/scripts/smoke-pi-attach.ts +1 -1
  141. package/scripts/smoke-user-scope-citizen.sh +1 -1
  142. package/scripts/tsconfig.json +1 -0
  143. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  144. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  145. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  146. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  147. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  148. package/scripts/check-acp-sdk-surface.ts +0 -275
  149. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  150. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  151. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  152. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -467
@@ -25,14 +25,14 @@
25
25
  * - Register the canonical `entwurf_v2` dispatch tool for existing garden citizens.
26
26
  * - Expose `entwurf_peers` facts for operator inspection (#50 C4: the socket-scan
27
27
  * `/entwurf-sessions` command is gone — the record listing is the only surface).
28
- * - Maintain the resident control socket used by v2 live-send / spawn-bg paths.
28
+ * - Maintain the resident control socket used by the v2 live-send path.
29
29
  * - Attach this pi session to its meta-record at session_start (#50 C2) and key
30
30
  * the control socket on the record's gardenId.
31
31
  *
32
32
  * Send-is-throw still applies at the control-socket protocol layer: a `send` RPC
33
33
  * ack confirms the receiver enqueued the message (`message_processed` semantics)
34
34
  * and does not wait for a peer turn result. Public v1 send surfaces were removed;
35
- * callers use `entwurf_v2`, whose decider chooses send / spawn-bg / mailbox.
35
+ * callers use `entwurf_v2`, whose decider chooses control-socket send / mailbox / native-push.
36
36
  *
37
37
  * Usage:
38
38
  * pi --entwurf-control (no id injection: pi owns its id, the record owns
@@ -758,8 +758,9 @@ async function handleCommand(
758
758
  // (entwurf-bridge entwurf_v2, the pi-native entwurf_v2 senderProvider via buildLocalSenderEnvelope)
759
759
  // pass the envelope structurally and never touch the message body — the
760
760
  // canonical XML-style payload is the shared formatSenderInfoBlock SSOT
761
- // (#50 C3: the dormant spawn-resume rail appends the same block to its
762
- // prompt, so both rails render one shape).
761
+ // (#50 C3: this is the ONE renderer since the visible-first cut removed the
762
+ // dormant spawn-resume rail that used to append the same block to a resume
763
+ // prompt — a future VISIBLE resume must render through it, not beside it).
763
764
  const senderInfoBlock = sender ? formatSenderInfoBlock(sender, wantsReply) : "";
764
765
 
765
766
  const mode = command.mode ?? "steer";
@@ -938,7 +939,8 @@ function updateStatus(ctx: ExtensionContext | null, enabled: boolean, gardenId:
938
939
  // is GONE with the id it mirrored. A name was a second place the address lived; the
939
940
  // record is the only one now, so there is nothing left to keep in sync and nothing
940
941
  // to crash over. (The dormant-resume authorization that leaned on the `entwurf` tag
941
- // moved to record existence see entwurf-v2-spawn-production.)
942
+ // moved to record existence, and that resume rail has since been withdrawn entirely;
943
+ // the record-authoritative remainder lives in resume-launch-identity.ts.)
942
944
 
943
945
  function updateSessionEnv(ctx: ExtensionContext | null, enabled: boolean, gardenId: string | null): void {
944
946
  if (!enabled || !gardenId) {
@@ -985,7 +987,7 @@ function getStringFlagFromArgv(flagName: string): string | undefined {
985
987
  }
986
988
 
987
989
  // `--emacs-agent-socket <name>` exports PI_EMACS_AGENT_SOCKET so this session's
988
- // own Bash/emacsclient calls (and any spawn-bg child that inherits this env)
990
+ // own Bash/emacsclient calls (and any child process that inherits this env)
989
991
  // target the right Emacs server socket — e.g. `emacsclient -s "$PI_EMACS_AGENT_SOCKET"`.
990
992
  // v2-only revival: the original ACP path injected this into the ACP child's spawn
991
993
  // env (acp-bridge.ts); with no ACP child on this branch, the consumer IS this pi
@@ -1042,6 +1044,8 @@ export default function (pi: ExtensionAPI) {
1042
1044
  if (shouldRegisterControlTools(pi)) {
1043
1045
  registerListSessionsTool(pi);
1044
1046
  registerEntwurfV2Tool(pi);
1047
+ registerFreshCallTool(pi);
1048
+ registerResumeCallTool(pi);
1045
1049
  }
1046
1050
 
1047
1051
  // The in-process mint refusals (`/new`, `/fork`, `/clone`, RPC new_session) are
@@ -1258,23 +1262,19 @@ interface EntwurfV2SurfaceModule {
1258
1262
  runAndRenderEntwurfV2FromSurface(
1259
1263
  params: {
1260
1264
  target: string;
1261
- intent: "fire-and-forget" | "owned-outcome";
1265
+ intent: "fire-and-forget";
1262
1266
  mode?: "steer" | "follow_up";
1263
1267
  wants_reply?: boolean;
1264
1268
  message: string;
1265
1269
  },
1266
- opts: {
1267
- senderProvider: () => SenderEnvelope | undefined;
1268
- agentDir?: string;
1269
- prefixRoots?: readonly string[];
1270
- },
1270
+ opts: { senderProvider: () => SenderEnvelope | undefined },
1271
1271
  ): Promise<{ text: string; isError: boolean }>;
1272
1272
  }
1273
1273
 
1274
1274
  function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1275
1275
  const entwurfV2Parameters = Type.Object({
1276
1276
  target: Type.String({ description: "Target garden id (use entwurf_peers to discover)" }),
1277
- intent: StringEnum(["fire-and-forget", "owned-outcome"] as const, {
1277
+ intent: StringEnum(["fire-and-forget"] as const, {
1278
1278
  description:
1279
1279
  "fire-and-forget = send/reply/hand-off to a LIVE socket target (currently backend pi) or to any " +
1280
1280
  "citizen with no socket liveness — the decider picks that citizen's rail, and a rail can also " +
@@ -1284,10 +1284,10 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1284
1284
  "dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate (two rejects, " +
1285
1285
  "not one). " +
1286
1286
  "Set wants_reply for an answer. " +
1287
- "owned-outcome = wake a DORMANT socket-domain citizen via spawn-bg resume ONLY on a live target " +
1288
- "rejected as owned-live-no-autosend, on self-fetch as backend-liveness-unsupported, on " +
1289
- "native-push as native-push-no-resume-authority (both lack resume authority, but the reasons " +
1290
- "differ because native-push IS probe-measured), and never auto-converted",
1287
+ "This is the ONLY intent. The second one, owned-outcome, resumed a dormant citizen by launching " +
1288
+ "a hidden background child and was withdrawn under the visible-first rule; it is not selectable, " +
1289
+ "not deprecated-but-tolerated, and a dormant citizen is currently unreachable by this verb and " +
1290
+ "rejects as dormant-fire-forget-unsupported.",
1291
1291
  }),
1292
1292
  message: Type.String({
1293
1293
  description:
@@ -1298,7 +1298,7 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1298
1298
  StringEnum(["steer", "follow_up"] as const, {
1299
1299
  description:
1300
1300
  "Injection style for a CONTROL-SOCKET send only: steer (immediate) or follow_up (after task). " +
1301
- "The mailbox, native-push, and spawn-bg plans carry no mode, so it has no effect on those rails.",
1301
+ "The mailbox and native-push plans carry no mode, so it has no effect on those rails.",
1302
1302
  }),
1303
1303
  ),
1304
1304
  wants_reply: Type.Optional(Type.Boolean({ description: "Human-conversation reply hint (default false)" })),
@@ -1306,7 +1306,7 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1306
1306
 
1307
1307
  type EntwurfV2Params = {
1308
1308
  target: string;
1309
- intent: "fire-and-forget" | "owned-outcome";
1309
+ intent: "fire-and-forget";
1310
1310
  message: string;
1311
1311
  mode?: "steer" | "follow_up";
1312
1312
  wants_reply?: boolean;
@@ -1324,24 +1324,24 @@ function registerEntwurfV2Tool(pi: ExtensionAPI): void {
1324
1324
  label: "Dispatch (v2)",
1325
1325
  description: `CANONICAL DELIVERY SURFACE for garden ids: message, reply, or hand off to whoever an id names. The id alone
1326
1326
  does not say which rail that citizen answers on. Give target + intent; the decider picks transport from
1327
- liveness (live socket citizen → control-socket send; dormant socket citizen → spawn-bg resume; deliverable
1328
- self-fetch citizen → meta-bridge mailbox; probe-alive native-push citizen → direct injection into its
1329
- conversation) and reports ONE outcome (delivered / rejected / lock-retained / delivered-but-lock-dirty).
1330
- EXISTING targets only; discover with entwurf_peers. INTENT picking wrong is rejected, never
1331
- auto-converted. A peer entwurf_peers shows as liveness=alive fire-and-forget. A citizen with NO socket
1332
- liveness (liveness=unsupported) is ALSO fire-and-forget unsupported means only "no control-socket probe" —
1333
- and the decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push
1334
- backend (e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the
1335
- mailbox delivers only to a DELIVERABLE citizen, so a terminated session, or a backend with no adapter here
1336
- (e.g. codex), is mailbox-undeliverable, not queued for an inbox nobody drains. The native-push probe is
1337
- 3-valued: alive injected; dead native-push-target-dead; indeterminate native-push-probe-indeterminate
1338
- (unestablished gone). owned-outcome wakes a DORMANT socket-domain citizen by spawn-bg resume ONLY — live
1339
- target owned-live-no-autosend, self-fetch backend-liveness-unsupported, native-push
1340
- native-push-no-resume-authority. LOCK: taken for a control-socket-DOMAIN dispatch the live send AND the
1341
- dormant cell's spawn-bg resume, a separate transport that still runs under that domain's lock. The mailbox
1342
- and native-push rails are lock-free — deliverability and the adapter probe guard them. mode applies to a
1343
- CONTROL-SOCKET send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000
1344
- chars; send an artifact path + digest for more.`,
1327
+ liveness (live socket citizen → control-socket send; deliverable self-fetch citizen → meta-bridge mailbox;
1328
+ probe-alive native-push citizen → direct injection into its conversation) and reports ONE outcome
1329
+ (delivered / rejected / delivered-but-lock-dirty). EXISTING targets only; discover with entwurf_peers.
1330
+ A peer entwurf_peers shows as liveness=alive → fire-and-forget. A citizen with NO socket liveness
1331
+ (liveness=unsupported) is ALSO fire-and-forget unsupported means only "no control-socket probe" — and the
1332
+ decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push backend
1333
+ (e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the mailbox
1334
+ delivers only to a DELIVERABLE citizen, so a terminated session, or a backend with no adapter here (e.g.
1335
+ codex), is mailbox-undeliverable, not queued for an inbox nobody drains. The native-push probe is 3-valued:
1336
+ alive injected; dead native-push-target-dead; indeterminate native-push-probe-indeterminate
1337
+ (unestablished gone). DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets
1338
+ dormant-fire-forget-unsupported same receiver rule as the mailbox, no active drainer means no
1339
+ delivery. The intent that used to answer there, owned-outcome, resumed it by
1340
+ launching a hidden background child; it was withdrawn under the visible-first rule, so re-open the session
1341
+ yourself and dispatch again. LOCK: taken for a control-socket-DOMAIN dispatch. The mailbox and native-push
1342
+ rails are lock-free — deliverability and the adapter probe guard them. mode applies to a CONTROL-SOCKET
1343
+ send only; other plans carry no mode. wants_reply rides every rail. message caps at 16000 chars; send an
1344
+ artifact path + digest for more.`,
1345
1345
  parameters: entwurfV2Parameters,
1346
1346
  async execute(
1347
1347
  _toolCallId: string,
@@ -1381,8 +1381,10 @@ chars; send an artifact path + digest for more.`,
1381
1381
  mode: params.mode,
1382
1382
  wants_reply: params.wants_reply,
1383
1383
  },
1384
- // agentDir / prefixRoots intentionally omitted here: the surface adapter falls back
1385
- // to the ENTWURF_PREFIX_ROOTS env SSOT for prefixRoots (5d-4); agentDir stays undefined.
1384
+ // No trust-preflight inputs are passed, and none exist to pass: the preflight on this
1385
+ // path guarded the resume verdict, so it left with `owned-outcome`. `senderProvider`
1386
+ // is the whole options surface now — do NOT re-add an `ENTWURF_PREFIX_ROOTS` fallback
1387
+ // here without a verdict that reads it (nothing on the dispatch path does).
1386
1388
  { senderProvider },
1387
1389
  );
1388
1390
  return {
@@ -1453,7 +1455,7 @@ function registerListSessionsTool(pi: ExtensionAPI): void {
1453
1455
  name: "entwurf_peers",
1454
1456
  label: "List Garden Citizens",
1455
1457
  description:
1456
- "List the entwurf fact surface: garden citizens from meta-records (including active self-fetch meta receivers such as claude-code) with liveness, plus diagnostics. The record is the sole address axis (#50 C4) — a control socket no record claims surfaces as a record-less-socket diagnostic, never a peer row. Pair with entwurf_v2 to address a peer by garden id; this surface reports facts, never per-row routing verbs.",
1458
+ "List the entwurf fact surface: garden citizens from meta-records (including active self-fetch meta receivers such as claude-code) with liveness, plus diagnostics. The record is the sole address axis (#50 C4) — a control socket no record claims surfaces as a record-less-socket diagnostic, never a peer row. Pair with entwurf_v2 to address a peer by garden id; this surface reports facts, never per-row routing verbs. A `dead` row is a REPORTED FACT and nothing more: that citizen is dormant and is currently unreachable by any verb, so listing it grants no action — appearing here is not an invitation to dispatch. It is facts-only and creates nothing: to open a NEW sibling use entwurf_fresh_call.",
1457
1459
  parameters: Type.Object({}),
1458
1460
  async execute(
1459
1461
  _toolCallId: string,
@@ -1478,3 +1480,179 @@ function registerListSessionsTool(pi: ExtensionAPI): void {
1478
1480
  },
1479
1481
  });
1480
1482
  }
1483
+
1484
+ // ============================================================================
1485
+ // Tool: entwurf_fresh_call
1486
+ // ============================================================================
1487
+
1488
+ const MUX_FRESH_CALL_MODULE = "./lib/mux-fresh-call.ts";
1489
+
1490
+ interface MuxFreshCallModule {
1491
+ freshCall(
1492
+ params: { backend: "pi" | "claude-code"; model: string; task: string; callerGardenId: string | null },
1493
+ env?: NodeJS.ProcessEnv,
1494
+ ): { ok: boolean };
1495
+ renderFreshCall(result: { ok: boolean }): { text: string; isError: boolean };
1496
+ }
1497
+
1498
+ /**
1499
+ * The caller identity for this surface is the RESIDENT's garden address — the one the record
1500
+ * minted at session start and keyed the control socket on. It is read from this extension's own
1501
+ * closure, never from a tool parameter and never from `process.env`.
1502
+ *
1503
+ * A caller that could pass an id could pass a WRONG one, and that value would become the address
1504
+ * the sibling calls back to — docs/mux-launch-rail.md §6-b keeps the measured incident. So there
1505
+ * is no parameter to be wrong with.
1506
+ *
1507
+ * A null resident id refuses LOUDLY rather than falling back: without an address to call back to,
1508
+ * the launch would be a window with no way home.
1509
+ */
1510
+ function registerFreshCallTool(pi: ExtensionAPI): void {
1511
+ // Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there.
1512
+ const registerTool = pi.registerTool as (def: any) => void;
1513
+ registerTool({
1514
+ name: "entwurf_fresh_call",
1515
+ label: "Open Fresh Sibling",
1516
+ description: `Open ONE fresh visible sibling in the operator's own tmux session and hand it a first task. Two fixed
1517
+ backends only: pi, claude-code. The sibling's FIRST action is a callback to you carrying a nonce, and the
1518
+ sender envelope of that callback is its garden id — that is how you learn the address of something that did
1519
+ not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus that nonce) and nothing else:
1520
+ it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls for the
1521
+ callback; if it never arrives the window is visible and can be read directly. For EXISTING citizens use
1522
+ entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the
1523
+ chosen runtime CLI (provider/model for pi; model id/alias for Claude Code); there are no arbitrary
1524
+ command/cwd/env knobs. Do not put secrets in the task — model and task argv are visible to same-user
1525
+ processes on this host.`,
1526
+ parameters: Type.Object({
1527
+ backend: StringEnum(["pi", "claude-code"], {
1528
+ description: "Which fixed runtime to open. Only these two; there is no arbitrary command.",
1529
+ }),
1530
+ model: Type.String({
1531
+ minLength: 1,
1532
+ maxLength: 200,
1533
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$",
1534
+ description: "Required runtime model: canonical provider/model for pi, or a Claude Code model id/alias.",
1535
+ }),
1536
+ task: Type.String({
1537
+ minLength: 1,
1538
+ maxLength: 16000,
1539
+ description:
1540
+ "What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
1541
+ }),
1542
+ }),
1543
+ async execute(
1544
+ _toolCallId: string,
1545
+ params: { backend: "pi" | "claude-code"; model: string; task: string },
1546
+ _signal: AbortSignal | undefined,
1547
+ _onUpdate: unknown,
1548
+ _ctx: ExtensionContext,
1549
+ ) {
1550
+ try {
1551
+ const mux = (await import(MUX_FRESH_CALL_MODULE)) as unknown as MuxFreshCallModule;
1552
+ const result = mux.freshCall({
1553
+ backend: params.backend,
1554
+ model: params.model,
1555
+ task: params.task,
1556
+ callerGardenId: residentGardenId,
1557
+ });
1558
+ const rendered = mux.renderFreshCall(result);
1559
+ return {
1560
+ content: [{ type: "text", text: rendered.text }],
1561
+ isError: rendered.isError,
1562
+ details: { isError: rendered.isError },
1563
+ };
1564
+ } catch (err) {
1565
+ const msg = err instanceof Error ? err.message : String(err);
1566
+ return {
1567
+ content: [{ type: "text", text: `entwurf_fresh_call error: ${msg}` }],
1568
+ isError: true,
1569
+ details: { error: msg },
1570
+ };
1571
+ }
1572
+ },
1573
+ });
1574
+ }
1575
+
1576
+ // ============================================================================
1577
+ // Tool: entwurf_resume_call
1578
+ // ============================================================================
1579
+
1580
+ const MUX_RESUME_CALL_MODULE = "./lib/mux-resume-call.ts";
1581
+ const V2_VISIBLE_RESUME_MODULE = "./lib/entwurf-v2-visible-resume.ts";
1582
+
1583
+ interface MuxResumeCallModule {
1584
+ resumeCall(
1585
+ params: { cwd: string; runtimeArgs: readonly string[] },
1586
+ env?: NodeJS.ProcessEnv,
1587
+ ): { ok: true; receipt: Record<string, string> } | { ok: false; reason: string };
1588
+ RESUME_CALL_REJECT_HINT: Record<string, string>;
1589
+ }
1590
+
1591
+ interface VisibleResumeModule {
1592
+ visibleResume(target: string, deps: unknown): Promise<{ ok: boolean }>;
1593
+ makeVisibleResumeDeps(launch: unknown): unknown;
1594
+ renderVisibleResume(result: { ok: boolean }): { text: string; isError: boolean };
1595
+ }
1596
+
1597
+ /**
1598
+ * Unlike `entwurf_fresh_call`, this tool needs NO caller identity: a resume names an existing
1599
+ * citizen, so the address is the parameter rather than something the sibling has to report back.
1600
+ * What it needs instead is the per-gid lock, which is why the composition lives on the v2 side of
1601
+ * the fence and only the LAUNCH is handed across (docs/mux-launch-rail.md §11).
1602
+ */
1603
+ function registerResumeCallTool(pi: ExtensionAPI): void {
1604
+ // Same TS2589 workaround as registerEntwurfV2Tool — see the comment block there.
1605
+ const registerTool = pi.registerTool as (def: any) => void;
1606
+ registerTool({
1607
+ name: "entwurf_resume_call",
1608
+ label: "Resume Dormant Citizen",
1609
+ description: `Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux
1610
+ session. The record supplies everything — which transcript, which model, which provider, which cwd — so the only
1611
+ input is the target id: there is no model override, no task, and no prompt. This runs NO turn: the window comes
1612
+ back with the conversation and waits, and talking to it is still entwurf_v2 fire-and-forget on the socket this
1613
+ call stands up. You get TWO receipts and they mean different things: a LAUNCH receipt (tmux made a window and was
1614
+ asked to start pi) and an OBSERVATION receipt (the control socket answered under the same id, or
1615
+ resume-unobserved). Unobserved is a real outcome, not an error to retry — the window is visible, so read it. A
1616
+ citizen that is already LIVE is refused: address it with entwurf_v2 instead. Only pi citizens have a same-id
1617
+ resume, because only they stand a control socket up.`,
1618
+ parameters: Type.Object({
1619
+ target: Type.String({
1620
+ minLength: 1,
1621
+ pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
1622
+ description: "Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).",
1623
+ }),
1624
+ }),
1625
+ async execute(
1626
+ _toolCallId: string,
1627
+ params: { target: string },
1628
+ _signal: AbortSignal | undefined,
1629
+ _onUpdate: unknown,
1630
+ _ctx: ExtensionContext,
1631
+ ) {
1632
+ try {
1633
+ const mux = (await import(MUX_RESUME_CALL_MODULE)) as unknown as MuxResumeCallModule;
1634
+ const v2 = (await import(V2_VISIBLE_RESUME_MODULE)) as unknown as VisibleResumeModule;
1635
+ const launch = (input: { cwd: string; runtimeArgs: readonly string[] }) => {
1636
+ const launched = mux.resumeCall(input);
1637
+ return launched.ok
1638
+ ? { ok: true, handle: launched.receipt }
1639
+ : { ok: false, reason: launched.reason, hint: mux.RESUME_CALL_REJECT_HINT[launched.reason] };
1640
+ };
1641
+ const result = await v2.visibleResume(params.target, v2.makeVisibleResumeDeps(launch));
1642
+ const rendered = v2.renderVisibleResume(result);
1643
+ return {
1644
+ content: [{ type: "text", text: rendered.text }],
1645
+ isError: rendered.isError,
1646
+ details: { isError: rendered.isError },
1647
+ };
1648
+ } catch (err) {
1649
+ const msg = err instanceof Error ? err.message : String(err);
1650
+ return {
1651
+ content: [{ type: "text", text: `entwurf_resume_call error: ${msg}` }],
1652
+ isError: true,
1653
+ details: { error: msg },
1654
+ };
1655
+ }
1656
+ },
1657
+ });
1658
+ }
@@ -602,6 +602,58 @@ export function actionableAcpBackendHint(message: string): string | undefined {
602
602
  ].join("\n");
603
603
  }
604
604
 
605
+ /**
606
+ * pi 0.84 streamSimple hook contract (#63; upstream pi-mono #7372 → doc-only PR
607
+ * #7576): implementations must invoke `options.onPayload` before sending the
608
+ * provider request and use any returned replacement.
609
+ *
610
+ * On this rail the provider request is the ACP `session/prompt` params — so the
611
+ * hook receives the EXACT `{sessionId, prompt}` object after the wire content is
612
+ * fully built (augment / reuse delta included) and immediately before
613
+ * `connection.prompt`. That is the truthful analogue of the built-in providers'
614
+ * post-build/pre-send boundary. Replacement is honored fail-closed, never
615
+ * silently: it must be a non-null, non-array object, keep the bootstrapped
616
+ * `sessionId`, and carry a non-empty prompt array — prompt rewriting is
617
+ * upstream-granted power, but entwurf cannot truthfully deliver to an ACP
618
+ * session it did not open, and an emptied prompt would undo this rail's own
619
+ * non-empty-prompt invariant; either refuses the turn before the wire.
620
+ *
621
+ * `options.onResponse` is an EXPLICIT LOCAL NON-HTTP EXEMPTION and is never
622
+ * invoked anywhere on this rail: pi hard-types it as HTTP `{status, headers}`
623
+ * (and the `after_provider_response` extension event re-emits exactly that), and
624
+ * ACP's terminal result arrives only AFTER the session-update body was already
625
+ * consumed — both the shape and the "before consuming its body" ordering are
626
+ * unmappable, so any call would fabricate HTTP evidence that does not exist.
627
+ * The absence is pinned behaviorally by check-acp-stream-hooks.
628
+ */
629
+ async function applyProviderPayloadHook<T extends { sessionId: string }>(
630
+ options: SimpleStreamOptions | undefined,
631
+ params: T,
632
+ model: Model<Api>,
633
+ ): Promise<T> {
634
+ const onPayload = options?.onPayload;
635
+ if (!onPayload) return params;
636
+ const replacement = await onPayload(params, model);
637
+ if (replacement === undefined) return params;
638
+ if (typeof replacement !== "object" || replacement === null || Array.isArray(replacement)) {
639
+ throw new Error(
640
+ "entwurf: before_provider_request returned a non-object replacement — the ACP prompt payload must stay a non-null, non-array object",
641
+ );
642
+ }
643
+ if ((replacement as { sessionId?: unknown }).sessionId !== params.sessionId) {
644
+ throw new Error(
645
+ "entwurf: before_provider_request changed the ACP sessionId — entwurf cannot truthfully deliver to a session it did not bootstrap; prompt rewriting is allowed, session identity is not",
646
+ );
647
+ }
648
+ const replacementPrompt = (replacement as { prompt?: unknown }).prompt;
649
+ if (!Array.isArray(replacementPrompt) || replacementPrompt.length === 0) {
650
+ throw new Error(
651
+ "entwurf: before_provider_request returned a replacement without a non-empty prompt array — an empty ACP prompt cannot be sent",
652
+ );
653
+ }
654
+ return replacement as T;
655
+ }
656
+
605
657
  /**
606
658
  * streamSimple for the entwurf provider. Returns the event stream
607
659
  * synchronously and drives the ACP turn on a microtask.
@@ -1038,17 +1090,22 @@ export function streamAcpTurn(
1038
1090
  // prompt could still sync-reject before the wire write; the next visible
1039
1091
  // event after this is the backend's own first token / tool notice.
1040
1092
  pushAcpLifecycleNotice(state, "sending prompt");
1093
+ // #63: the pi streamSimple payload hook sees the EXACT wire params and may
1094
+ // replace them (fail-closed integrity inside the helper). It runs while the
1095
+ // bootstrap abort listener is still armed; the recheck below keeps an abort
1096
+ // raised during a slow handler ahead of the wire write.
1097
+ const wireParams = await applyProviderPayloadHook(options, { sessionId: acpSessionId, prompt }, model);
1098
+ if (signal?.aborted) throw new Error("aborted during payload hook");
1041
1099
  // Hand the abort window over to the prompt driver: from here on an abort
1042
1100
  // is a protocol `session/cancel` first, teardown only after the grace.
1043
1101
  if (signal && onAbort) {
1044
1102
  signal.removeEventListener("abort", onAbort);
1045
1103
  onAbort = undefined;
1046
1104
  }
1047
- const promptResult = await awaitAcpPromptTurn(
1048
- session,
1049
- { sessionId: acpSessionId, prompt },
1050
- { signal, graceMs: deps.abortGraceMs ?? ABORT_CANCEL_GRACE_MS },
1051
- );
1105
+ const promptResult = await awaitAcpPromptTurn(session, wireParams, {
1106
+ signal,
1107
+ graceMs: deps.abortGraceMs ?? ABORT_CANCEL_GRACE_MS,
1108
+ });
1052
1109
 
1053
1110
  session.activePromptHandler = undefined;
1054
1111
  session.busy = false;
@@ -1109,13 +1166,15 @@ export function streamAcpTurn(
1109
1166
 
1110
1167
  // S2f visibility: about to send the delta to the resident child.
1111
1168
  pushAcpLifecycleNotice(state, "sending prompt");
1112
- // A reuse turn has no bootstrap window at all — the prompt driver owns
1113
- // the whole abort surface (protocol cancel first, teardown after grace).
1114
- const promptResult = await awaitAcpPromptTurn(
1115
- session,
1116
- { sessionId: session.acpSessionId, prompt },
1117
- { signal, graceMs: deps.abortGraceMs ?? ABORT_CANCEL_GRACE_MS },
1118
- );
1169
+ // #63: same hook boundary as a new turn — the reuse delta is the wire
1170
+ // params here. The recheck keeps an abort raised during a slow handler
1171
+ // ahead of the wire write; the prompt driver then owns the abort surface.
1172
+ const wireParams = await applyProviderPayloadHook(options, { sessionId: session.acpSessionId, prompt }, model);
1173
+ if (signal?.aborted) throw new Error("aborted during payload hook");
1174
+ const promptResult = await awaitAcpPromptTurn(session, wireParams, {
1175
+ signal,
1176
+ graceMs: deps.abortGraceMs ?? ABORT_CANCEL_GRACE_MS,
1177
+ });
1119
1178
 
1120
1179
  session.activePromptHandler = undefined;
1121
1180
  session.busy = false;
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // claude-agent-acp's SettingsManager loads the operator's `~/.claude/settings.json`
4
4
  // DIRECTLY (CLAUDE_CONFIG_DIR is the only knob that redirects that read). So the
5
- // operator's native `permissions.defaultMode` ("auto"), hooks, plugins, and
6
- // per-cwd memory/projects state would otherwise leak into entwurf ACP
5
+ // operator's native `permissions.defaultMode` (whatever its current value), hooks,
6
+ // plugins, and per-cwd memory/projects state would otherwise leak into entwurf ACP
7
7
  // sessions. The overlay redirects SettingsManager at a pi-owned directory whose
8
8
  // `settings.json` WE author (minimal, `hooks:{}`), while keeping exactly the
9
9
  // operator entries a backend needs (credentials, caches, built-in skills)
@@ -105,9 +105,11 @@ export const OVERLAY_BINARY_OWNED: ReadonlySet<string> = new Set([".claude.json"
105
105
 
106
106
  /**
107
107
  * Minimal overlay settings.json. Only fields with a reason to pin:
108
- * - `permissions.defaultMode: "default"` neutralizes the operator's native
109
- * "auto"; combined with the explicit `tools`/`permissionAllow` surface,
110
- * "default" auto-passes every tool we expose without prompts.
108
+ * - `permissions.defaultMode: "bypassPermissions"` is deliberate unattended
109
+ * ACP operation: a tool call must never suspend a model turn on an interactive
110
+ * permission prompt. This does not widen the callable surface — explicit
111
+ * `tools`/`disallowedTools` still shape it, and `permissionAllow` still rides
112
+ * the inline Claude settings — or bypass backend authentication.
111
113
  * - `autoMemoryEnabled: false` — SDK opt-out for auto-memory (defense in
112
114
  * depth; the tiny non-empty engraving/preset replacement is the primary
113
115
  * write-containment lever for Claude ACP).
@@ -117,7 +119,7 @@ export const OVERLAY_BINARY_OWNED: ReadonlySet<string> = new Set([".claude.json"
117
119
  export function overlaySettingsJson(): string {
118
120
  return `${JSON.stringify(
119
121
  {
120
- permissions: { defaultMode: "default" },
122
+ permissions: { defaultMode: "bypassPermissions" },
121
123
  autoMemoryEnabled: false,
122
124
  hooks: {},
123
125
  },
@@ -37,12 +37,14 @@ export interface SenderEnvelope {
37
37
 
38
38
  /**
39
39
  * THE `<sender_info>` synthesis — the one place the sender envelope becomes
40
- * message text. Two consumers, one shape (#50 C3):
40
+ * message text. ONE consumer since the visible-first cut (#50 C3):
41
41
  * - the live socket rail's RECEIVER (entwurf-control handleCommand("send")
42
- * appends it to the delivered customMessage);
43
- * - the dormant spawn-resume rail's SENDER (entwurf-v2-production appends it
44
- * to the resume prompt, so a resumed citizen wakes knowing who called in
45
- * exactly the shape a live delivery would have rendered).
42
+ * appends it to the delivered customMessage).
43
+ *
44
+ * The second consumer was the dormant spawn-resume rail's SENDER, which appended
45
+ * the same shape to a resume prompt so a resumed citizen woke knowing who called.
46
+ * That rail is gone. The shape stays single-sourced here so a future VISIBLE
47
+ * resume renders an identical envelope rather than inventing a second one.
46
48
  * `wants_reply` is emitted only when explicitly true — an unset/false marker
47
49
  * renders nothing (etiquette marker, not transport contract).
48
50
  */
@@ -1,19 +1,18 @@
1
1
  /**
2
2
  * entwurf-core — sync entwurf execution, host-agnostic.
3
3
  *
4
- * DIRECT importers, MEASURED from the import graph (2026-07-27) — not from memory:
4
+ * DIRECT importers, RE-MEASURED from the import graph (2026-08-06) — not from memory:
5
5
  * - pi-extensions/entwurf-control.ts (the pi adapter)
6
- * - pi-extensions/lib/entwurf-v2-spawn-production.ts (v2 spawn-bg launch identity)
7
- * - pi-extensions/lib/project-trust-handler.ts · lib/acp/augment.ts ·
8
- * lib/native-push/adapter.ts
9
- * - plus gates/smokes (check-shell-quote, check-package-source-routing, )
6
+ * - pi-extensions/lib/resume-launch-identity.ts (record-authoritative launch identity,
7
+ * itself consumer-zero since the visible-first cut see its header)
8
+ * - plus gates/smokes (check-shell-quote, check-package-source-routing,
9
+ * check-entwurf-session-identity, new-session-id, resolve-acp-bridge)
10
10
  *
11
- * The MCP bridge is a TRANSITIVE consumer, not a direct one, and the distinction is the
12
- * whole point: `mcp/entwurf-bridge/src/index.ts → entwurf-v2-surface.ts →
13
- * entwurf-v2-production.ts → entwurf-v2-spawn-production.ts → entwurf-core.ts`. So the old
14
- * header's claim — that the bridge imports this shared implementation the way a v1 tool
15
- * surface did — was false, but so is "the bridge does not reach it": it reaches it at
16
- * runtime, only for spawn-bg production.
11
+ * The MCP bridge does NOT reach this module on any path, and that changed with the cut:
12
+ * its one edge ran `index.ts → entwurf-v2-surface.ts → entwurf-v2-production.ts →
13
+ * entwurf-v2-spawn-production.ts → entwurf-core.ts`, and the spawn-production link was
14
+ * deleted with the `spawn-bg` transport. Do not restate the old "transitive consumer,
15
+ * for spawn-bg production" line; re-measure before claiming either way.
17
16
  *
18
17
  * NOT consumers on any path: `pi-extensions/entwurf.ts` (the v1 pi native tool surface) and
19
18
  * `pi-extensions/lib/entwurf-async.ts` were REMOVED in the 0.12 cutover. Do not reintroduce
@@ -131,9 +130,9 @@ export function normalizeCodexEntwurfModelForAcp(model?: string): string | undef
131
130
  // The Entwurf Target Registry is GONE (#50 C3). `pi/entwurf-targets.json` and its
132
131
  // reader chain (loadEntwurfTargets / resolveEntwurfTarget / EntwurfRegistryError +
133
132
  // the ~/.pi/agent symlink machinery) were the v1 "narrow door" for spawn-model
134
- // policy — but v2 never spawns from a model tuple: entwurf_v2 resumes an
135
- // already-identified record-backed citizen, and the model axis is the citizen's
136
- // own (Identity Preservation Rule). The last readers were the RT-dead
133
+ // policy — but v2 never spawns at all, from a model tuple or otherwise: entwurf_v2
134
+ // addresses an already-identified record-backed citizen, and the model axis is the
135
+ // citizen's own (Identity Preservation Rule). The last readers were the RT-dead
137
136
  // buildSessionName mirror and the v1 spawn guard, both swept with this cut.
138
137
  // Bridge-extension routing for provider=entwurf survives below (getRegistryRouting
139
138
  // ← scripts/resolve-acp-bridge.ts) and takes a caller-supplied target — no file.
@@ -211,7 +210,8 @@ const SESSION_READ_CHUNK_BYTES = 64 * 1024;
211
210
  * old name-mirror integrity check and the `requireEntwurf` name-tag
212
211
  * authorization are gone (#50 C3): resume authorization is record existence,
213
212
  * and transcript integrity is the caller's header-id ↔ record.nativeSessionId
214
- * check (entwurf-v2-spawn-production.resolveResumeLaunchIdentity).
213
+ * check (resume-launch-identity.resolveResumeLaunchIdentity — the leaf that
214
+ * outlived the spawn-bg factory it used to sit in).
215
215
  */
216
216
  export interface RecordedSessionIdentity {
217
217
  /** JSONL header `id` (pi's own session id — the record's `nativeSessionId`). */