@junghanacs/entwurf 0.41.2 → 0.50.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 (122) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +328 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/ENTWURF-CLI.json +1229 -0
  6. package/ENTWURF.json +284 -0
  7. package/FAQ.md +4 -4
  8. package/README.md +125 -184
  9. package/VERIFY.md +2 -2
  10. package/docs/external-mcp-host.md +3 -3
  11. package/docs/setup-clean-host.md +37 -12
  12. package/mcp/entwurf-bridge/dist/ENTWURF.json +284 -0
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
  30. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  31. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  33. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  34. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  35. package/mcp/entwurf-bridge/src/index.ts +149 -327
  36. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  37. package/native/win32-x64/manifest.json +1 -1
  38. package/package.json +8 -4
  39. package/pi/pi-durable/bootstrap.mjs +179 -6
  40. package/pi/pi-durable/carrier/runtime.js +108 -9
  41. package/pi/pi-durable/carrier/session-name.js +11 -0
  42. package/pi/pi-durable/carrier/sessions.js +96 -20
  43. package/pi/pi-durable/overlay/upstream-pin.json +11 -5
  44. package/pi-extensions/lib/agent-guidance.ts +341 -0
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
  46. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
  47. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  48. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  49. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  50. package/pi-extensions/lib/meta-session.ts +13 -8
  51. package/pi-extensions/lib/mux-fresh-call.ts +23 -26
  52. package/pi-extensions/lib/mux-resume-call.ts +95 -23
  53. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
  54. package/pi-extensions/lib/pi-durable-location.ts +446 -0
  55. package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
  56. package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
  57. package/pi-extensions/lib/remote-pair-send.ts +7 -4
  58. package/pi-extensions/lib/resume-launch-identity.ts +31 -12
  59. package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
  60. package/run.sh +417 -654
  61. package/scripts/build-bridge.sh +10 -0
  62. package/scripts/check-capability-bundle-reach.ts +86 -0
  63. package/scripts/check-claude-launch.ts +341 -0
  64. package/scripts/check-entwurf-self-address.ts +3 -3
  65. package/scripts/check-entwurf-v2-surface.ts +94 -98
  66. package/scripts/check-fresh-call-dispatch.ts +70 -9
  67. package/scripts/check-fresh-cut-gate.sh +23 -9
  68. package/scripts/check-gate-qualification.ts +24 -17
  69. package/scripts/check-herdr-activation.ts +301 -23
  70. package/scripts/check-herdr-fresh-call.ts +161 -25
  71. package/scripts/check-herdr-plugin-build.ts +173 -1
  72. package/scripts/check-herdr-plugin.ts +28 -2
  73. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  74. package/scripts/check-install-surface.ts +1236 -6
  75. package/scripts/check-meta-doctor-oracle.sh +1 -1
  76. package/scripts/check-meta-facts.ts +20 -1
  77. package/scripts/check-mux-resume-call.ts +48 -3
  78. package/scripts/check-peer-facts.ts +39 -2
  79. package/scripts/cli-help.mjs +222 -0
  80. package/scripts/dev-bin.sh +39 -2
  81. package/scripts/herdr-activation.mjs +28 -6
  82. package/scripts/herdr-operator-link.mjs +389 -0
  83. package/scripts/herdr-plugin-activate.mjs +5 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  85. package/scripts/herdr-rails.mjs +43 -7
  86. package/scripts/herdr-runtime.mjs +27 -0
  87. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  88. package/scripts/meta-bridge-prune.ts +1 -1
  89. package/scripts/meta-facts.ts +1 -1
  90. package/scripts/mutants/agent-guidance.json +160 -0
  91. package/scripts/mutants/bridge-boot-resume.json +6 -8
  92. package/scripts/mutants/capability-bundle-reach.json +124 -0
  93. package/scripts/mutants/claude-launch.json +142 -0
  94. package/scripts/mutants/cli-help-source.json +420 -0
  95. package/scripts/mutants/control-send-receipt.json +21 -4
  96. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  97. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  98. package/scripts/mutants/fresh-cut.json +15 -0
  99. package/scripts/mutants/herdr-activation.json +128 -0
  100. package/scripts/mutants/herdr-fresh-call.json +278 -5
  101. package/scripts/mutants/herdr-operator-link.json +318 -0
  102. package/scripts/mutants/herdr-plugin-build.json +55 -0
  103. package/scripts/mutants/herdr-plugin.json +17 -0
  104. package/scripts/mutants/herdr-resume-call.json +288 -0
  105. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  106. package/scripts/mutants/meta-facts.json +15 -0
  107. package/scripts/mutants/mux-fresh-call.json +14 -3
  108. package/scripts/mutants/mux-resume-call.json +89 -3
  109. package/scripts/mutants/peer-facts.json +15 -0
  110. package/scripts/mutants/pi-durable.json +996 -6
  111. package/scripts/mutants/remote-pair-send.json +21 -2
  112. package/scripts/mutants/self-address.json +10 -10
  113. package/scripts/mutants/source-install.json +35 -0
  114. package/scripts/mutants/v2-surface.json +50 -64
  115. package/scripts/mutants/v2-visible-resume.json +266 -6
  116. package/scripts/peer-facts.ts +1 -1
  117. package/scripts/smoke-agy-install-state.sh +66 -0
  118. package/scripts/smoke-meta-prune.sh +23 -0
  119. package/scripts/smoke-omp-fresh-live.ts +12 -3
  120. package/scripts/smoke-omp-receive-live.ts +7 -1
  121. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  122. package/team.json.example +39 -0
@@ -12,7 +12,7 @@
12
12
  "windowsTargetPlatformVersion": "10.0.26100.0",
13
13
  "visualStudio": {
14
14
  "displayName": "Visual Studio Enterprise 2026",
15
- "installationVersion": "18.11.12315.225"
15
+ "installationVersion": "18.11.12328.437"
16
16
  }
17
17
  },
18
18
  "source": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.41.2",
4
- "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request and reopen pi or Claude Code in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
3
+ "version": "0.50.0",
4
+ "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request, visibly reopen supported sessions under the same id, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Junghan Kim",
@@ -39,6 +39,9 @@
39
39
  ],
40
40
  "type": "module",
41
41
  "files": [
42
+ "ENTWURF.json",
43
+ "ENTWURF-CLI.json",
44
+ "team.json.example",
42
45
  "protocol.js",
43
46
  "pi-extensions/",
44
47
  "native/win32-x64/",
@@ -158,6 +161,7 @@
158
161
  "check-copilot-statusline": "./run.sh check-copilot-statusline",
159
162
  "check-copilot-launch": "./run.sh check-copilot-launch",
160
163
  "check-pi-launch": "./run.sh check-pi-launch",
164
+ "check-claude-launch": "./run.sh check-claude-launch",
161
165
  "smoke-copilot-statusline-state": "./run.sh smoke-copilot-statusline-state",
162
166
  "smoke-copilot-mcp-state": "./run.sh smoke-copilot-mcp-state",
163
167
  "smoke-omp-bridge-state": "./run.sh smoke-omp-bridge-state",
@@ -197,8 +201,8 @@
197
201
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
198
202
  "check:toolchain": "pnpm lint && pnpm typecheck",
199
203
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior && ./run.sh check-remote-pair-installed-audit && ./run.sh check-chain-terminus-publication",
200
- "check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-control-retire && ./run.sh check-pi-retire-request && ./run.sh check-cc-retire-pidfd && ./run.sh check-cc-retire-request && ./run.sh check-retirement-event && ./run.sh check-entwurf-v2-retire && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-win32-preview && ./run.sh check-pack-win32-carrier && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-fresh-call-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-native-push-paused && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module && ./run.sh check-remote-pair-beside",
201
- "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
204
+ "check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-control-retire && ./run.sh check-pi-retire-request && ./run.sh check-cc-retire-pidfd && ./run.sh check-cc-retire-request && ./run.sh check-retirement-event && ./run.sh check-entwurf-v2-retire && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-win32-preview && ./run.sh check-pack-win32-carrier && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-fresh-call-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-native-push-paused && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-agent-guidance-beside && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module && ./run.sh check-remote-pair-beside",
205
+ "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-claude-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
202
206
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-pack-argv-matcher && ./run.sh check-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
203
207
  },
204
208
  "pi": {
@@ -14,7 +14,9 @@
14
14
  // fails and closes itself retires that doorbell at once;
15
15
  // - a launcher's first input, when given, is admitted to the root ONCE, after that birth;
16
16
  // - the TUI is handed the native view with the garden id on every conversation label
17
- // (`withGardenIdentity`).
17
+ // (`withGardenIdentity`);
18
+ // - every open takes the owner fence (#157 B3) — built once here from the compiled closure — inside
19
+ // the session directory before its native lease; a refusal opens nothing.
18
20
  // Nothing of the native registry, controller or TUI is rebuilt here.
19
21
  //
20
22
  // The durable app itself is this package's CARRIER (./carrier/, emitted from Entwurf's owned source
@@ -27,6 +29,8 @@
27
29
  // <entwurf>/pi/pi-durable/bootstrap.mjs \
28
30
  // --provider <provider> --model <model id> --width task-wide [--entwurf-bootstrap <json>]
29
31
  // ... bootstrap.mjs --continue
32
+ // ... bootstrap.mjs --resume
33
+ // ... bootstrap.mjs --session <native id>
30
34
  //
31
35
  // Explicit means the native API, not a vendor argv: upstream's durable main takes only
32
36
  // `--continue`, so `--provider`/`--model` reach pi's own `findInitialAgentModel` through the
@@ -45,9 +49,17 @@
45
49
  // address rides the process env only.
46
50
  // `--continue` opens the newest session for this directory as upstream does (not a chosen
47
51
  // record), so every flag that only a new session can honour is refused beside it by name.
52
+ // `--session <native id>` (#157 G3 B-L1) reopens exactly that native session of this directory as it
53
+ // was saved, through the owned runtime's `session` option, with no interactive session picker or
54
+ // picker-specific TTY check; the normal TUI still runs. The id is the native session directory name, never a garden id: a native CLI selector, not an address. It is
55
+ // held to the owned `isSessionName` grammar before operator-native module evaluation, TUI load, open
56
+ // or birth; that scope is lexical (the selection stays under this cwd's session root by name), not
57
+ // physical symlink confinement. A well-formed id this directory does not hold is the runtime's own
58
+ // refusal, after the TUI module loads and before any TUI runs. `--continue`, `--resume` and
59
+ // `--session` are mutually exclusive, and every new-session flag is refused beside each of them.
48
60
  //
49
- // `--native-module <absolute file>` (#130 P2) names ONE operator module, for a new or a continued
50
- // session. It is an ordinary ES module: its own top-level evaluation (top-level await included) is
61
+ // `--native-module <absolute file>` (#130 P2) names ONE operator module, for a new, a continued, a
62
+ // resumed or a named session. It is an ordinary ES module: its own top-level evaluation (top-level await included) is
51
63
  // its initialization, and its default export is a native durable Extension, handed to `openDurable`
52
64
  // by reference after the contact. Initialization runs before the TUI or the runtime is imported —
53
65
  // the runtime creates the provider runtime before it installs any extension (carrier/runtime.js
@@ -68,16 +80,28 @@
68
80
  // REPLACE the first extension or keep both tools. A refusal at module k leaves modules before it
69
81
  // evaluated: nothing is rolled back. No watch, no recursion, nothing persisted.
70
82
  //
83
+ // `--entwurf-adopt <json>` (#157 B3 I-3) opens the one native session a parent reserved for this process. The
84
+ // payload is closed: exactly `{v:1, gardenId, metaRoot, sourceGeneration}`, and any other key or value is refused. It
85
+ // is exclusive with `--continue`, `--resume`, `--session` and every new-session flag. The garden record under
86
+ // `metaRoot` is the address. Its native session id is the session, held to the same grammar as `--session`.
87
+ // Before anything loads, the four `ENTWURF_META_*_DIR` values this process actually received must be `metaRoot`'s
88
+ // standard children (the launch's fixed projection). The contact's roots are those four, whatever a seam passes.
89
+ // The open takes the adoption fence of that context, which re-reads the record and its location inside its own
90
+ // holds. The selection read here is never handed to the fence as evidence.
91
+ //
71
92
  // Without that resolver the carrier's first pi-coding-agent internal import fails by name (an
72
93
  // unsupported entwurf-pi-dist: URL scheme), and that failure is the honest one.
73
94
  import { readdirSync, realpathSync, statSync } from "node:fs";
74
95
  import { isAbsolute, join } from "node:path";
75
96
  import { fileURLToPath, pathToFileURL } from "node:url";
97
+ import { readAddressableMetaIdentity } from "../../mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js";
98
+ import { createPiDurableOwnerFence } from "../../mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js";
76
99
  import { decodeOmpBootstrapPayload } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js";
77
100
  import {
78
101
  closeAll,
79
102
  createPiDurableContact,
80
103
  failAfterCleanup,
104
+ PI_DURABLE_BACKEND,
81
105
  PI_DURABLE_EXTENSION_NAME,
82
106
  PI_DURABLE_TOOL_NAMES,
83
107
  piDurableGardenIds,
@@ -87,6 +111,14 @@ import {
87
111
  const RUNTIME = "./carrier/runtime.js";
88
112
  const TUI = "./carrier/tui.js";
89
113
  const SESSIONS = "./carrier/sessions.js";
114
+ /** The session-name grammar alone: no imports and no I/O, so reading it loads nothing an operator module may prepare. */
115
+ const SESSION_NAME = "./carrier/session-name.js";
116
+
117
+ /**
118
+ * The owner fence every ordinary open of this process hands the runtime (#157 B3); an adoption hands its own (below).
119
+ * Building it reads and writes nothing.
120
+ */
121
+ const OWNER_FENCE = createPiDurableOwnerFence();
90
122
 
91
123
  /** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
92
124
  export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
@@ -102,9 +134,20 @@ const VALUE_FLAGS = [
102
134
  "--entwurf-bootstrap",
103
135
  "--native-module",
104
136
  "--native-module-dir",
137
+ "--session",
138
+ "--entwurf-adopt",
105
139
  ];
106
140
  /** Flags only a NEW session can honour. */
107
141
  const FRESH_ONLY_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
142
+ /** The closed adopt context's keys, sorted: nothing else rides it (#157 B3 I-3). */
143
+ const ADOPT_KEYS = ["gardenId", "metaRoot", "sourceGeneration", "v"];
144
+ /** Each contact root of an adoption: the env name its launch carries it in, and its standard child of `metaRoot`. */
145
+ const ADOPT_ROOTS = {
146
+ sessionsDir: ["ENTWURF_META_SESSIONS_DIR", "meta-sessions"],
147
+ mailboxDir: ["ENTWURF_META_MAILBOX_DIR", "meta-mailbox"],
148
+ sendersDir: ["ENTWURF_META_SENDERS_DIR", "meta-senders"],
149
+ receiversDir: ["ENTWURF_META_RECEIVERS_DIR", "meta-receivers"],
150
+ };
108
151
 
109
152
  /**
110
153
  * Extension names the durable app installs itself — the contact, then the carrier's own
@@ -124,6 +167,68 @@ function refuse(reason, detail) {
124
167
 
125
168
  const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
126
169
 
170
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
171
+
172
+ /**
173
+ * `--entwurf-adopt`'s closed context, decoded before anything loads. The key set and `v` are this grammar's own.
174
+ * The garden id, the generation and the absolute root are the adoption fence's checks, run here by building the
175
+ * fence, which reads and writes nothing.
176
+ */
177
+ function decodeAdoptContext(text) {
178
+ let value;
179
+ try {
180
+ value = JSON.parse(text);
181
+ } catch (error) {
182
+ throw Object.assign(refuse("adopt-context-invalid", "--entwurf-adopt is not JSON"), { cause: error });
183
+ }
184
+ if (
185
+ typeof value !== "object" ||
186
+ value === null ||
187
+ Array.isArray(value) ||
188
+ Object.keys(value).sort().join(",") !== ADOPT_KEYS.join(",")
189
+ ) {
190
+ throw refuse("adopt-context-invalid", "--entwurf-adopt is exactly {v, gardenId, metaRoot, sourceGeneration}");
191
+ }
192
+ if (value.v !== 1) throw refuse("adopt-context-invalid", "--entwurf-adopt is version 1");
193
+ const { gardenId, metaRoot, sourceGeneration } = value;
194
+ let fence;
195
+ try {
196
+ fence = createPiDurableOwnerFence({ adoption: { gardenId, metaRoot, sourceGeneration } });
197
+ } catch (error) {
198
+ throw Object.assign(refuse("adopt-context-invalid", messageOf(error)), { cause: error });
199
+ }
200
+ return { gardenId, metaRoot, sourceGeneration, fence };
201
+ }
202
+
203
+ /** An adoption's contact roots: the standard children of its `metaRoot`, the shape the parent's reserve required. */
204
+ function adoptRoots(metaRoot) {
205
+ return Object.fromEntries(Object.entries(ADOPT_ROOTS).map(([key, [, child]]) => [key, join(metaRoot, child)]));
206
+ }
207
+
208
+ /** The four meta roots this process actually received must be the context's; no ambient root stands in for them. */
209
+ function requireAdoptRootEnv(metaRoot) {
210
+ const mismatched = Object.values(ADOPT_ROOTS)
211
+ .filter(([name, child]) => process.env[name] !== join(metaRoot, child))
212
+ .map(([name]) => name);
213
+ if (mismatched.length > 0) {
214
+ throw refuse("adopt-root-mismatch", `${mismatched.join(", ")} must be the standard children of ${metaRoot}`);
215
+ }
216
+ }
217
+
218
+ /** The native session the garden record names. The record is the address, read through the addressable reader. */
219
+ function adoptedSession({ gardenId, metaRoot }) {
220
+ let record;
221
+ try {
222
+ record = readAddressableMetaIdentity(gardenId, join(metaRoot, ADOPT_ROOTS.sessionsDir[1]));
223
+ } catch (error) {
224
+ throw Object.assign(refuse("adopt-record-unaddressable", `${gardenId}: ${messageOf(error)}`), { cause: error });
225
+ }
226
+ if (record.backend !== PI_DURABLE_BACKEND) {
227
+ throw refuse("adopt-record-not-pi-durable", `${gardenId} is a ${record.backend} record`);
228
+ }
229
+ return record.nativeSessionId;
230
+ }
231
+
127
232
  /** The identity carriers present now: names and values, compared in memory and never printed. */
128
233
  function identitySnapshot() {
129
234
  return Object.fromEntries(
@@ -291,6 +396,8 @@ async function loadNativeModules(files) {
291
396
  * checkout-only gates, which drive a private bundle) passes another one — never the launch argv.
292
397
  * `extensions` are the operator's native extensions (`--native-module` or `--native-module-dir`),
293
398
  * installed after the contact, by reference.
399
+ * `adopt` is a decoded `--entwurf-adopt` context (#157 B3 I-3). Its fence admits the open, in place of the ordinary
400
+ * one, and its `metaRoot`'s four standard roots are the contact's, spread last so no seam replaces them.
294
401
  *
295
402
  * @param {{
296
403
  * bridgeEntry?: string,
@@ -301,7 +408,8 @@ async function loadNativeModules(files) {
301
408
  * firstInput?: string,
302
409
  * extensions?: readonly object[],
303
410
  * contact?: Record<string, unknown>,
304
- * open?: (options: { cwd: string, continueSession: boolean, session?: string, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
411
+ * adopt?: { gardenId: string, metaRoot: string, sourceGeneration: string, fence: object },
412
+ * open?: (options: { cwd: string, continueSession: boolean, session?: string, extensions: readonly object[], model?: { provider: string, model: string }, fence: object }) => Promise<any>,
305
413
  * }} options
306
414
  * @returns {Promise<{ durable: any, contact: any, attachment: { gardenId: string } & Record<string, any>, firstInput?: any, close(): Promise<void> }>}
307
415
  * (stated, because the compiled closure carries no declarations: a cleanup path that rethrows
@@ -316,6 +424,7 @@ export async function openDurableCitizen({
316
424
  firstInput,
317
425
  extensions = [],
318
426
  contact: contactOptions = {},
427
+ adopt,
319
428
  open,
320
429
  }) {
321
430
  if (continueSession && (model !== undefined || firstInput !== undefined)) {
@@ -337,7 +446,12 @@ export async function openDurableCitizen({
337
446
  throw refuse("first-input-empty", "a first input is a non-empty task");
338
447
  }
339
448
  const openDurable = open ?? (await import(RUNTIME)).openDurable;
340
- const contact = createPiDurableContact({ ...contactOptions, bridgeEntry });
449
+ const fence = adopt === undefined ? OWNER_FENCE : adopt.fence;
450
+ const contact = createPiDurableContact({
451
+ ...contactOptions,
452
+ bridgeEntry,
453
+ ...(adopt === undefined ? {} : adoptRoots(adopt.metaRoot)),
454
+ });
341
455
  let durable;
342
456
  try {
343
457
  durable = await openDurable({
@@ -346,6 +460,7 @@ export async function openDurableCitizen({
346
460
  ...(session === undefined ? {} : { session }),
347
461
  extensions: [contact.extension, ...extensions],
348
462
  ...(model === undefined ? {} : { model }),
463
+ fence,
349
464
  });
350
465
  } catch (error) {
351
466
  contact.fail(error);
@@ -426,7 +541,42 @@ function parseArgs(argv) {
426
541
  "--resume chooses a session and --continue opens the newest one; give one of them",
427
542
  );
428
543
  }
544
+ const session = values.get("--session");
545
+ if (session !== undefined && continueSession) {
546
+ throw refuse(
547
+ "session-continue-exclusive",
548
+ "--session names the session to reopen and --continue opens the newest one; give one of them",
549
+ );
550
+ }
551
+ if (session !== undefined && resume) {
552
+ throw refuse(
553
+ "session-resume-exclusive",
554
+ "--session names the session to reopen and --resume chooses one from a list; give one of them",
555
+ );
556
+ }
429
557
  const fresh = FRESH_ONLY_FLAGS.filter((flag) => values.has(flag));
558
+ const adoptArg = values.get("--entwurf-adopt");
559
+ if (adoptArg !== undefined) {
560
+ const others = [
561
+ ...(continueSession ? ["--continue"] : []),
562
+ ...(resume ? ["--resume"] : []),
563
+ ...(session === undefined ? [] : ["--session"]),
564
+ ...fresh,
565
+ ];
566
+ if (others.length > 0) {
567
+ throw refuse(
568
+ "adopt-exclusive",
569
+ `${others.join(", ")} cannot be given with --entwurf-adopt: an adoption opens exactly the reserved session`,
570
+ );
571
+ }
572
+ }
573
+ const adopt = adoptArg === undefined ? undefined : decodeAdoptContext(adoptArg);
574
+ if (session !== undefined && fresh.length > 0) {
575
+ throw refuse(
576
+ "fresh-only-argument-with-session",
577
+ `${fresh.join(", ")} apply only to a new session; --session reopens the named session as it was saved`,
578
+ );
579
+ }
430
580
  if (resume && fresh.length > 0) {
431
581
  throw refuse(
432
582
  "fresh-only-argument-with-resume",
@@ -455,6 +605,8 @@ function parseArgs(argv) {
455
605
  : {};
456
606
  if (continueSession) return { continueSession, ...nativeModule };
457
607
  if (resume) return { resume, ...nativeModule };
608
+ if (session !== undefined) return { session, ...nativeModule };
609
+ if (adopt !== undefined) return { adopt, ...nativeModule };
458
610
  const provider = values.get("--provider");
459
611
  if (provider === undefined) {
460
612
  throw refuse(
@@ -543,8 +695,29 @@ async function chooseSession(cwd, sessionsDir, pick) {
543
695
  * }} [seams]
544
696
  */
545
697
  export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry, pick } = {}) {
546
- // Every argument refusal is decided here, before any module, the TUI or the app loads.
698
+ // Every argument refusal but the `--session` grammar is decided here, before any module, the TUI or
699
+ // the app loads.
547
700
  const { nativeModules = [], resume = false, ...args } = parseArgs(argv);
701
+ // `--entwurf-adopt` (#157 B3 I-3): the roots this process received are checked against the context before
702
+ // anything else is read. The session is the one the garden record names, under the `--session` grammar below.
703
+ if (args.adopt !== undefined) {
704
+ requireAdoptRootEnv(args.adopt.metaRoot);
705
+ args.session = adoptedSession(args.adopt);
706
+ }
707
+ // `--session` is held to the owned session-name grammar the runtime selects by, before operator-native
708
+ // module evaluation, TUI load, open or birth. Only the import-free grammar file is read for it — never
709
+ // the session store, whose SDK config import fixes package state from the environment an operator
710
+ // module may still prepare. The shape is lexical — it names a directory under this cwd's session root
711
+ // by name only, which is not physical symlink confinement.
712
+ if (args.session !== undefined) {
713
+ const { isSessionName } = await import(SESSION_NAME);
714
+ if (!isSessionName(args.session)) {
715
+ throw refuse(
716
+ "session-id-malformed",
717
+ `${JSON.stringify(args.session)} is not a durable session id (<13-digit ms>-<uuid>)`,
718
+ );
719
+ }
720
+ }
548
721
  // The caller's directory, fixed before any operator code runs and handed to the app explicitly.
549
722
  const cwd = process.cwd();
550
723
  const extensions = await loadNativeModules(nativeModules);
@@ -1,6 +1,11 @@
1
1
 
2
2
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
3
- import { clampThinkingLevel, getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
3
+ import {
4
+ clampThinkingLevel,
5
+ cleanupSessionResources,
6
+ getSupportedThinkingLevels,
7
+
8
+ } from "@earendil-works/pi-ai";
4
9
  import {
5
10
 
6
11
 
@@ -26,7 +31,7 @@ import {
26
31
  ExecutionEnvs,
27
32
  findInitialAgentModel,
28
33
  } from "./harness-setup.js";
29
- import { selectSession } from "./sessions.js";
34
+ import { selectSession } from "./sessions.js";
30
35
  import { Subagent } from "./subagent.js";
31
36
 
32
37
  const context = BACKGROUND_CONTEXT;
@@ -96,6 +101,8 @@ const context = BACKGROUND_CONTEXT;
96
101
 
97
102
 
98
103
 
104
+
105
+
99
106
 
100
107
 
101
108
 
@@ -144,7 +151,16 @@ function titleOf(entry ) {
144
151
  return { title: text.replace(/\s+/g, " ").trim() };
145
152
  }
146
153
 
147
- export async function openDurable(options = {}) {
154
+ /** What failed, for an owner end's `cause`. */
155
+ function causeOf(error ) {
156
+ return error instanceof Error ? error.message : String(error);
157
+ }
158
+
159
+ export async function openDurable(options ) {
160
+ // The type requires it; a JS caller is refused here, before the session root is created.
161
+ if (options.fence === undefined) {
162
+ throw new Error("fence-required: an owned durable open takes the owner fence before its native lease");
163
+ }
148
164
  if (options.model !== undefined && (options.continueSession || options.session !== undefined)) {
149
165
  throw new Error("An explicit model applies only to a new session, not to a continued one");
150
166
  }
@@ -152,12 +168,16 @@ export async function openDurable(options = {})
152
168
  throw new Error("A chosen session and the newest one are two different sessions; name one");
153
169
  }
154
170
  const location = await selectSession(
171
+ options.fence,
155
172
  options.cwd ?? process.cwd(),
156
173
  options.continueSession ?? false,
157
174
  options.session,
158
175
  );
176
+ const ticket = location.ticket;
159
177
  const envs = new ExecutionEnvs(location.cwd);
160
178
  let harness ;
179
+ // The storage handle Harness.open is handed: if Harness.open itself rejects, this is what was opened.
180
+ let backendHandle ;
161
181
  try {
162
182
  const modelRuntime = await ModelRuntime.create();
163
183
  const settingsManager = SettingsManager.create(location.cwd);
@@ -169,8 +189,12 @@ export async function openDurable(options = {})
169
189
 
170
190
  const pendingReports = [];
171
191
  let report = (error) => pendingReports.push(error);
192
+ // Recorded BEFORE the call: once the open is attempted, no later outcome proves that nothing was opened.
193
+ ticket.storageOpenAttempted = true;
194
+ const backend = await openNodeSqliteStorage(location.database);
195
+ backendHandle = backend;
172
196
  harness = await Harness.open(
173
- await openNodeSqliteStorage(location.database),
197
+ backend,
174
198
  {
175
199
  models: modelRuntime,
176
200
  registry,
@@ -180,6 +204,7 @@ export async function openDurable(options = {})
180
204
  },
181
205
  context,
182
206
  );
207
+ await ticket.storageOpened();
183
208
  const initial = location.created
184
209
  ? await findInitialAgentModel(settingsManager, modelRuntime, options.model)
185
210
  : undefined;
@@ -439,20 +464,94 @@ export async function openDurable(options = {})
439
464
  unsubscribeCommits();
440
465
  conversation.dispose();
441
466
  closeTasks();
467
+ // The resource end the fence records (#157 B3). Storage is `closed` only when the first actual close
468
+ // resolved, no failure was seen before it, and the Session's own terminal result is not `failed`.
469
+ const failures = [];
470
+ let storage = "failed";
471
+ let envCleanup ;
472
+ let lease = "released";
442
473
  try {
443
474
  // Close writes no outcome: a running turn resumes with --continue.
444
475
  await opened.close(context);
445
- await envs.cleanup(context);
476
+ const ended = await opened.closed;
477
+ if (state.failure === undefined && ended.reason !== "failed") storage = "closed";
478
+ try {
479
+ await envs.cleanup(context);
480
+ } catch (error) {
481
+ envCleanup = "failed";
482
+ failures.push(error);
483
+ }
484
+ } catch (error) {
485
+ failures.push(error);
446
486
  } finally {
447
- await location.release();
487
+ // The provider connections the turns left open for reuse: pi-ai keeps a Codex WebSocket per provider
488
+ // session on a five-minute idle timer, and pi's AgentSession releases them on dispose. Without the same
489
+ // release here a closed host stays alive until that timer (#157 S3 LIVE-R1). No id: the provider session
490
+ // ids are docs of the storage this process alone opened, and a host process holds this one Session.
491
+ try {
492
+ cleanupSessionResources();
493
+ } catch (error) {
494
+ failures.push(error);
495
+ }
496
+ try {
497
+ await location.release();
498
+ } catch (error) {
499
+ lease = "failed";
500
+ failures.push(error);
501
+ }
448
502
  }
503
+ const failure = failures[0] ?? (storage === "closed" ? undefined : "the session failed before close");
504
+ const record = {
505
+ storage,
506
+ lease,
507
+ ...(envCleanup === undefined ? {} : { envCleanup }),
508
+ ...(failure === undefined ? {} : { cause: causeOf(failure) }),
509
+ };
510
+ // An env cleanup failure is the caller's error, not the resource's: the end stays `closed`.
511
+ try {
512
+ if (storage === "closed" && lease === "released") await ticket.closed(record);
513
+ else await ticket.closeFailed(record);
514
+ } catch (error) {
515
+ failures.push(error);
516
+ }
517
+ if (failures.length === 1) throw failures[0];
518
+ if (failures.length > 1) throw new AggregateError(failures, "The durable session did not close cleanly");
449
519
  })();
450
520
  return closing;
451
521
  },
452
522
  };
453
523
  } catch (error) {
454
- await harness?.close(context).catch(() => {});
455
- await location.release().catch(() => {});
456
- throw error;
524
+ // An init failure (#157 B3). Only a storage open that was never attempted, with the lease verifiably released,
525
+ // ends `closed`; once attempted, neither a settled Harness close nor a released lease proves the resource ended.
526
+ let storage = ticket.storageOpenAttempted ? "unknown" : "never-opened";
527
+ // The init error first, then every cleanup failure: none is dropped.
528
+ const failures = [error];
529
+ // The first close of what was opened: the Harness once it opened, else the storage handle Harness.open was
530
+ // handed and rejected. A close that settles is no verified close here; one that rejects is a failed storage.
531
+ const closable = harness ?? backendHandle;
532
+ if (closable !== undefined) {
533
+ try {
534
+ await closable.close(context);
535
+ } catch (closeError) {
536
+ storage = "failed";
537
+ failures.push(closeError);
538
+ }
539
+ }
540
+ let lease = "released";
541
+ try {
542
+ await location.release();
543
+ } catch (releaseError) {
544
+ lease = "failed";
545
+ failures.push(releaseError);
546
+ }
547
+ const ending = { storage, lease, cause: failures.map(causeOf).join("; ") };
548
+ try {
549
+ if (storage === "never-opened" && lease === "released") await ticket.closed(ending);
550
+ else await ticket.closeFailed(ending);
551
+ } catch (fenceError) {
552
+ failures.push(fenceError);
553
+ }
554
+ if (failures.length === 1) throw error;
555
+ throw new AggregateError(failures, `${causeOf(error)}; its cleanup or owner end failed too`);
457
556
  }
458
557
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The durable session name grammar (#157 G3 B-L1), alone in its own file: no imports and no I/O, so the
3
+ * bootstrap can hold a `--session` value to it before an operator module prepares the environment, without
4
+ * loading the session store, the SDK config or a lock library first. sessions.ts selects by this same predicate.
5
+ */
6
+ const SESSION_NAME = /^\d{13}-[0-9a-f-]{36}$/u;
7
+
8
+ /** Whether `name` has the exact shape of a session directory name: `<13-digit ms>-<uuid>`. */
9
+ export function isSessionName(name ) {
10
+ return SESSION_NAME.test(name);
11
+ }