@junghanacs/entwurf 0.40.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +55 -2
  3. package/CHANGELOG.md +562 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +18 -2
  6. package/FAQ.md +8 -4
  7. package/README.md +60 -45
  8. package/VERIFY.md +14 -11
  9. package/demo/README.md +1 -1
  10. package/docs/acp-backend-rail.md +6 -2
  11. package/docs/external-mcp-host.md +8 -6
  12. package/docs/fresh-cut-policy.md +9 -1
  13. package/docs/setup-clean-host.md +76 -20
  14. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +81 -19
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +96 -16
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +5 -29
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +28 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +26 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-retire-request.js +76 -0
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +67 -17
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
  33. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
  34. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  35. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  36. package/mcp/entwurf-bridge/src/index.ts +93 -19
  37. package/mcp/entwurf-bridge/test.sh +2 -2
  38. package/mcp/entwurf-bridge/tsconfig.build.json +10 -1
  39. package/package.json +15 -14
  40. package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
  41. package/pi/pi-durable/carrier/tui.js +18 -3
  42. package/pi/pi-durable/overlay/upstream-pin.json +12 -12
  43. package/pi-extensions/entwurf-control.ts +55 -20
  44. package/pi-extensions/lib/acp/backend-adapter.ts +5 -3
  45. package/pi-extensions/lib/acp/backend.ts +12 -9
  46. package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  47. package/pi-extensions/lib/cc-retire-request.ts +553 -0
  48. package/pi-extensions/lib/control-retire.ts +195 -0
  49. package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
  50. package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
  51. package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
  52. package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
  53. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
  54. package/pi-extensions/lib/fresh-call-composition.ts +15 -0
  55. package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
  56. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  57. package/pi-extensions/lib/meta-session.ts +140 -16
  58. package/pi-extensions/lib/mux-fresh-call.ts +7 -27
  59. package/pi-extensions/lib/mux-launch.ts +29 -0
  60. package/pi-extensions/lib/mux-resume-call.ts +33 -12
  61. package/pi-extensions/lib/pi-retire-request.ts +111 -0
  62. package/pi-extensions/lib/resume-launch-identity.ts +104 -18
  63. package/pi-extensions/lib/retirement-event.ts +253 -0
  64. package/pi-extensions/meta-bridge-hook.ts +142 -13
  65. package/pi-extensions/meta-bridge-pi-durable.ts +57 -3
  66. package/run.sh +123 -34
  67. package/scripts/agy-bridge-config.py +4 -1
  68. package/scripts/build-bridge.sh +14 -0
  69. package/scripts/check-capability-bundle-reach.ts +135 -2
  70. package/scripts/check-entwurf-bridge-boot.ts +2 -1
  71. package/scripts/check-entwurf-fact-provider.ts +1 -0
  72. package/scripts/check-entwurf-v2-production.ts +2 -0
  73. package/scripts/check-entwurf-v2-surface.ts +7 -0
  74. package/scripts/check-gate-qualification.ts +14 -8
  75. package/scripts/check-herdr-activation.ts +2 -2
  76. package/scripts/check-herdr-placement.ts +1 -0
  77. package/scripts/check-hook-launch-topology.ts +6 -15
  78. package/scripts/check-install-container.sh +2 -2
  79. package/scripts/check-meta-doctor-oracle.sh +27 -1
  80. package/scripts/check-meta-facts.ts +8 -3
  81. package/scripts/check-meta-hook-session-switch.ts +230 -13
  82. package/scripts/check-meta-identity-consumers.ts +1 -0
  83. package/scripts/check-meta-listing.ts +1 -0
  84. package/scripts/check-meta-manifest-schema.py +1 -1
  85. package/scripts/check-meta-session.ts +31 -0
  86. package/scripts/check-meta-v3-record.ts +55 -1
  87. package/scripts/check-mux-resume-call.ts +35 -9
  88. package/scripts/check-peer-facts.ts +1 -0
  89. package/scripts/check-pi-durable-contact.ts +3 -1
  90. package/scripts/check-pi-durable-send.ts +4 -2
  91. package/scripts/check-pi-mcp-bridge.ts +1 -0
  92. package/scripts/check-resume-launch-identity.ts +62 -23
  93. package/scripts/lib/codex-fresh-live-protocol.ts +3 -2
  94. package/scripts/lib/meta-hook-bundle.ts +72 -0
  95. package/scripts/meta-bridge-claude-floor.sh +13 -9
  96. package/scripts/meta-bridge-doctor.sh +17 -0
  97. package/scripts/meta-bridge-install.sh +6 -0
  98. package/scripts/meta-facts.ts +1 -1
  99. package/scripts/mutants/bridge-boot-resume.json +5 -2
  100. package/scripts/mutants/cc-retire-pidfd.json +129 -0
  101. package/scripts/mutants/cc-retire-request.json +171 -0
  102. package/scripts/mutants/control-retire.json +105 -0
  103. package/scripts/mutants/control-send-receipt.json +1 -1
  104. package/scripts/mutants/copilot-receive.json +2 -6
  105. package/scripts/mutants/entwurf-v2-retire.json +476 -0
  106. package/scripts/mutants/fresh-call-dispatch.json +37 -0
  107. package/scripts/mutants/meta-hook-session-switch.json +88 -0
  108. package/scripts/mutants/meta-identity.json +57 -0
  109. package/scripts/mutants/mux-fresh-call.json +1 -1
  110. package/scripts/mutants/mux-resume-call.json +40 -4
  111. package/scripts/mutants/pack-install.json +4 -4
  112. package/scripts/mutants/pi-durable.json +139 -0
  113. package/scripts/mutants/pi-floor.json +5 -5
  114. package/scripts/mutants/pi-retire-request.json +72 -0
  115. package/scripts/mutants/resume-launch-identity.json +35 -8
  116. package/scripts/mutants/retirement-event.json +195 -0
  117. package/scripts/mutants/v2-surface.json +16 -0
  118. package/scripts/mutants/v2-visible-resume.json +283 -13
  119. package/scripts/probe-bridge-command.ts +1 -0
  120. package/scripts/smoke-agy-install-state.sh +2 -2
  121. package/scripts/smoke-codex-fresh-live.ts +2 -1
  122. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  123. package/scripts/smoke-herdr-raw-install-live.sh +15 -7
  124. package/scripts/smoke-meta-install-state.sh +4 -4
  125. package/scripts/smoke-mux-lifecycle-live.ts +10 -10
  126. package/scripts/smoke-omp-mcp-state.sh +1 -1
  127. package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
  128. package/scripts/smoke-pi-provider-state.sh +3 -3
@@ -31,16 +31,18 @@
31
31
  * seat, #105; nothing is ever created), optionally at ONE literal requested
32
32
  * cwd (cross-repo fresh, #73); returns a LAUNCH receipt only, and the new
33
33
  * address arrives later as the sender envelope of the sibling's nonce callback.
34
- * - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
35
- * window; target-only, runs no turn, LAUNCH and OBSERVATION receipts stay
36
- * apart.
34
+ * - entwurf_resume_call — reopen ONE DORMANT pi or Claude Code citizen under its OWN garden id
35
+ * in a visible window; target-only, submits no prompt, LAUNCH and OBSERVATION
36
+ * receipts stay apart, the caller is attributed but grants nothing.
37
+ * - entwurf_retire_call — one operator-requested target, one retirement attempt; actor is not
38
+ * permission, and action / append-record / lock-release receipts stay separate.
37
39
  * - entwurf_callback — ZERO-ARGUMENT fresh-sibling callback. Reads ENTWURF_CALLBACK_TARGET +
38
40
  * ENTWURF_CALLBACK_NONCE from this process env (launcher-injected), validates
39
41
  * grammar, and dispatches through the existing v2 runner. Refuses by name
40
42
  * when env is absent/malformed or this process is a Codex-provenance bridge.
41
43
  * No model-supplied target fallback.
42
44
  *
43
- * That list is the WHOLE public surface — eight verbs — and `check-entwurf-bridge-boot`
45
+ * That list is the WHOLE public surface — nine verbs — and `check-entwurf-bridge-boot`
44
46
  * (G1f) holds it as an exact set on the runtime tools/list, so a verb added or dropped
45
47
  * here without a decision is red rather than merely undocumented.
46
48
  *
@@ -78,6 +80,8 @@ import { ENTWURF_PEERS_RENDER_LIMIT, renderEntwurfPeers } from "../../../pi-exte
78
80
  import { computeSelfAddressability, type MetaDeliveryDomain } from "../../../pi-extensions/lib/entwurf-self-address.ts";
79
81
  import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.ts";
80
82
  import { resolveMailboxWakeModeCapability } from "../../../pi-extensions/lib/entwurf-v2-decider.ts";
83
+ import { retireCitizen } from "../../../pi-extensions/lib/entwurf-v2-retire.ts";
84
+ import { makeProductionRetireDeps } from "../../../pi-extensions/lib/entwurf-v2-retire-production.ts";
81
85
  import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.ts";
82
86
  import {
83
87
  makeVisibleResumeDeps,
@@ -818,7 +822,7 @@ server.tool(
818
822
  // biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above
819
823
  .regex(/^[A-Za-z0-9][A-Za-z0-9._/:\[\]-]*$/)
820
824
  .describe(
821
- "Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.",
825
+ "Required runtime model: canonical provider/model for pi (a bare alias is refused), provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name. Neither pi nor pi-durable may name the openrouter provider.",
822
826
  ),
823
827
  task: z
824
828
  .string()
@@ -904,7 +908,7 @@ server.tool(
904
908
  // resume` cannot open a window on its own and `mux-resume-call` cannot read a record — that is
905
909
  // the import fence in docs/mux-launch-rail.md §11, expressed as a function argument.
906
910
  const visibleResumeLaunch: VisibleResumeDeps["launch"] = (input) => {
907
- const launched = resumeCall({ cwd: input.cwd, runtimeArgs: input.runtimeArgs });
911
+ const launched = resumeCall({ backend: input.backend, cwd: input.cwd, runtimeArgs: input.runtimeArgs });
908
912
  return launched.ok
909
913
  ? { ok: true, handle: launched.receipt }
910
914
  : { ok: false, reason: launched.reason, hint: RESUME_CALL_REJECT_HINT[launched.reason] };
@@ -912,27 +916,45 @@ const visibleResumeLaunch: VisibleResumeDeps["launch"] = (input) => {
912
916
 
913
917
  server.tool(
914
918
  "entwurf_resume_call",
915
- "Reopen ONE DORMANT pi citizen under its OWN garden id, in a visible window in the operator's own tmux " +
916
- "session. The record supplies everything — which transcript, which model, which provider, which cwd — so " +
917
- "the only input is the target id: there is no model override, no task, and no prompt. This runs NO turn: " +
918
- "the window comes back with the conversation and waits, and talking to it is still entwurf_v2 " +
919
- "fire-and-forget on the socket this call stands up. You get TWO receipts and they mean different things: a " +
920
- "LAUNCH receipt (tmux made a window and was asked to start pi) and an OBSERVATION receipt (the control " +
921
- "socket answered under the same id, or resume-unobserved). Unobserved is a real outcome, not an error to " +
922
- "retry — the window is visible, so read it. A citizen that is already LIVE is refused: address it with " +
923
- "entwurf_v2 instead. Only pi citizens have a same-id resume, because only they stand a control socket up. " +
924
- "Requires that this agent itself runs inside tmux.",
919
+ "Reopen ONE DORMANT pi or Claude Code citizen under its OWN garden id, in a visible window in the operator's " +
920
+ "own tmux session. The record supplies everything — which session, which cwd, and for pi which transcript, " +
921
+ "model and provider — so the only input is the target id: there is no model override, no task, and no " +
922
+ "prompt, and a Claude Code session comes back as `claude --resume <its session id>` with none of the model " +
923
+ "or permission flags it was once started with. This verb submits NO prompt; what the runtime itself does " +
924
+ "after it comes back (for Claude Code, its hook re-arms the doorbell and unread mail may then wake a turn) " +
925
+ "is not measured, and talking to it is still entwurf_v2 fire-and-forget. You get TWO receipts and they mean " +
926
+ "different things: a LAUNCH receipt (tmux made a window and was asked to start the runtime) and an " +
927
+ "OBSERVATION receipt (pi: the control socket answered under the same id; Claude Code: a NEW live process in " +
928
+ "the window this call opened re-armed its receiver marker for the same session with a fresh arm binding the " +
929
+ "record also carries; or resume-unobserved; or record-unreadable with its cause). Unobserved is a real " +
930
+ "outcome, not an error to retry — the window is visible, so read it. A citizen that is already LIVE is " +
931
+ "refused: address it with entwurf_v2 instead. A Claude Code citizen is reopened only when the last validated " +
932
+ "owner the Entwurf hook armed for its session is proven gone AND that arm is still the record's last one; " +
933
+ "no marker, an unreadable or mismatched one, an inconclusive owner or an unbound one is refused, and a " +
934
+ "process holding the session without a committed hook attach is invisible to that proof. Other backends " +
935
+ "have no same-id resume. Requires that this agent itself runs inside tmux (inside herdr it is refused).",
925
936
  {
926
937
  target: z
927
938
  .string()
928
939
  .min(1)
929
940
  .regex(/^\d{8}T\d{6}-[0-9a-f]{6}$/)
930
- .describe("Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers)."),
941
+ .describe("Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers)."),
931
942
  },
932
- async ({ target }) => {
943
+ async ({ target }, extra) => {
944
+ // Who asked, from the same authoritative caller identity fresh_call reads. Attribution only:
945
+ // an anonymous caller is reported as unattributed, never refused, and a record-backed one is
946
+ // granted nothing the target's record and live facts do not already decide.
947
+ let actor: string | null = null;
948
+ try {
949
+ actor = (await buildAuthoritativeSelfEnvelope({ requestMeta: extra._meta })).envelope.sessionId;
950
+ } catch (err) {
951
+ if (!(err instanceof EntwurfEnvelopeWiringError)) {
952
+ return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
953
+ }
954
+ }
933
955
  try {
934
956
  const result = await visibleResume(target, makeVisibleResumeDeps(visibleResumeLaunch));
935
- const rendered = renderVisibleResume(result);
957
+ const rendered = renderVisibleResume(result, { actor });
936
958
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
937
959
  } catch (err) {
938
960
  return textErr(`entwurf_resume_call error: ${err instanceof Error ? err.message : String(err)}`);
@@ -940,6 +962,58 @@ server.tool(
940
962
  },
941
963
  );
942
964
 
965
+ server.tool(
966
+ "entwurf_retire_call",
967
+ "Only when the human operator explicitly requested this particular target/work: make ONE retirement attempt for that request. " +
968
+ "Development work, a role or a record-backed actor is NOT a grant. Use host approval where provided/configured; " +
969
+ "Pi and pi-durable have no built-in per-call approval prompt by default. An explicit operator request and " +
970
+ "agent operating policy are required; attribution and MCP annotations are hints, not grants or proof of approval. " +
971
+ "Never use for autonomous cleanup or timers. Never automatically retry an unknown, cancelled or lost result. " +
972
+ "Target must be an existing Pi or Claude Code garden id; self retirement refuses. Pi shutdown-requested is " +
973
+ "only an ACK, not process exit, flush or model silence. Claude Code exited means that fd-pinned process only, " +
974
+ "not conversation end, children or flush. Do not manually /resume or /clear the target during the request. " +
975
+ "The JSON receipt preserves outcome, record and release separately; returning a receipt does not mean the " +
976
+ "target exited. Cancellation can suppress the wire response without undoing an action. For plural explicit " +
977
+ "requests, invoke once for each named target; never infer a roster or expand the request.",
978
+ {
979
+ target: z
980
+ .string()
981
+ .min(1)
982
+ .regex(/^\d{8}T\d{6}-[0-9a-f]{6}$/)
983
+ .describe("Exact garden id of the Pi or Claude Code target the human operator explicitly requested to retire."),
984
+ },
985
+ { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
986
+ async ({ target }, extra) => {
987
+ try {
988
+ const receipt = await retireCitizen(
989
+ target,
990
+ makeProductionRetireDeps({
991
+ resolveCaller: async () => (await resolveAuthoritativeSender({ requestMeta: extra._meta }))?.envelope ?? null,
992
+ signal: extra.signal,
993
+ }),
994
+ );
995
+ // isError reports uncertainty/faults, NOT a common Pi-ACK/CC-exit success boundary.
996
+ // Named refusals are completed decisions. Always preserve the full three-axis JSON.
997
+ const outcome = receipt.outcome;
998
+ const uncertain =
999
+ outcome.kind === "unknown" ||
1000
+ outcome.kind === "preflight-error" ||
1001
+ (outcome.kind === "pi" && outcome.result.boundary === "unknown") ||
1002
+ (outcome.kind === "claude-code" && outcome.result.outcome === "unknown");
1003
+ const recordFault =
1004
+ receipt.record.status === "requested-append-error" || receipt.record.status === "outcome-append-error";
1005
+ const releaseFault = receipt.release.status !== "released" && receipt.release.status !== "not-acquired";
1006
+ const json = JSON.stringify(receipt);
1007
+ return uncertain || recordFault || releaseFault ? textErr(json) : textOk(json);
1008
+ } catch (err) {
1009
+ return textErr(
1010
+ `entwurf_retire_call error: ${err instanceof Error ? err.message : String(err)}. ` +
1011
+ "The outcome may be unknown; this does not prove no action. Do not automatically retry.",
1012
+ );
1013
+ }
1014
+ },
1015
+ );
1016
+
943
1017
  server.tool(
944
1018
  "entwurf_callback",
945
1019
  "ZERO-ARGUMENT callback for a fresh sibling this process was launched as. Reads " +
@@ -55,7 +55,7 @@ names=set(raw)
55
55
  # was an artifact-surface hole: a bundle missing entwurf_fresh_call / entwurf_resume_call
56
56
  # answered tools/list and passed. Equality also refuses an undecided extra verb; the
57
57
  # duplicate arm catches a name registered twice, which set membership cannot see.
58
- expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read','entwurf_register_native','entwurf_fresh_call','entwurf_resume_call','entwurf_callback'}
58
+ expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read','entwurf_register_native','entwurf_fresh_call','entwurf_resume_call','entwurf_retire_call','entwurf_callback'}
59
59
  legacy={'entwurf','entwurf_resume','entwurf_send'} & names
60
60
  if len(raw)!=len(names):
61
61
  raise SystemExit(f"duplicate tool registrations: {sorted(raw)}")
@@ -66,7 +66,7 @@ if legacy:
66
66
  if names!=expected:
67
67
  raise SystemExit(f"tool set MISMATCH — missing {sorted(expected-names)}, unexpected {sorted(names-expected)}")
68
68
  PY
69
- ok "public tool surface is EXACTLY the eight garden verbs (no v1 verb, no undecided extra, no duplicate)"
69
+ ok "public tool surface is EXACTLY the nine garden verbs (no v1 verb, no undecided extra, no duplicate)"
70
70
 
71
71
  SELF_JSON="$(printf '%s\n' "$OUT" | grep '"id":3' | tail -1)"
72
72
  [[ "$SELF_JSON" == *"isError"* ]] || fail "entwurf_self without identity should be an error: $SELF_JSON"
@@ -147,6 +147,12 @@
147
147
  // remote-pair-send, entwurf-v2-surface, meta-session) are ALREADY emitted for the bridge's own
148
148
  // entwurf_v2 handler, so this adds ONE leaf JS and ZERO new deps. It is never in index.ts's boot
149
149
  // closure.
150
+ // WHY cc-retire-request rides THIS build (#147 G3): the core-side Claude Code retire adapter
151
+ // must run from the compiled bridge as well as from source. It was first supplied as a private
152
+ // entry; the #147 public retirement factory now reaches it too. Keep the named entry for the
153
+ // standalone compiled adapter consumer proof. Its helper is not TS: the
154
+ // build script byte-copies pi-extensions/lib/cc-retire-pidfd.py beside the emitted adapter.
155
+ // Its repo imports (meta-session, entwurf-v2-visible-resume) are ALREADY emitted for the bridge.
150
156
  "include": [
151
157
  "./src/index.ts",
152
158
  "../../protocol.js",
@@ -168,6 +174,9 @@
168
174
  "../../pi-extensions/meta-bridge-hook-codex.ts",
169
175
  "../../pi-extensions/meta-bridge-omp.ts",
170
176
  "../../pi-extensions/meta-bridge-receive-omp.ts",
171
- "../../pi-extensions/meta-bridge-pi-durable.ts"
177
+ "../../pi-extensions/meta-bridge-pi-durable.ts",
178
+ "../../pi-extensions/lib/cc-retire-request.ts",
179
+ // Keep the standalone Pi adapter consumer entry; the public factory also reaches it now.
180
+ "../../pi-extensions/lib/pi-retire-request.ts"
172
181
  ]
173
182
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.40.0",
4
- "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, reopen pi in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
3
+ "version": "0.41.1",
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.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Junghan Kim",
@@ -89,27 +89,28 @@
89
89
  "CHANGELOG.md",
90
90
  "!**/__pycache__",
91
91
  "!**/*.pyc",
92
- "!pi-extensions/**/*.test.ts"
92
+ "!pi-extensions/**/*.test.ts",
93
+ "!pi-extensions/**/fixtures/**"
93
94
  ],
94
95
  "engines": {
95
96
  "node": ">=24.0.0"
96
97
  },
97
98
  "entwurf": {
98
- "claudeCodeFloor": ">=2.1.217"
99
+ "claudeCodeFloor": ">=2.1.306"
99
100
  },
100
101
  "dependencies": {
101
102
  "@agentclientprotocol/claude-agent-acp": "0.90.0",
102
103
  "@agentclientprotocol/sdk": "1.8.0",
103
104
  "@anthropic-ai/sdk": "0.100.1",
104
- "@earendil-works/chord": "1.3.0",
105
- "@earendil-works/pi-agent-core": "1.3.0",
106
- "@earendil-works/pi-ai": "1.3.0",
107
- "@earendil-works/pi-codemode": "1.3.0",
108
- "@earendil-works/pi-coding-agent": "1.3.0",
109
- "@earendil-works/pi-durable": "1.3.0",
110
- "@earendil-works/pi-mcp": "1.3.0",
111
- "@earendil-works/pi-telemetry": "1.3.0",
112
- "@earendil-works/pi-tui": "1.3.0",
105
+ "@earendil-works/chord": "1.4.0",
106
+ "@earendil-works/pi-agent-core": "1.4.0",
107
+ "@earendil-works/pi-ai": "1.4.0",
108
+ "@earendil-works/pi-codemode": "1.4.0",
109
+ "@earendil-works/pi-coding-agent": "1.4.0",
110
+ "@earendil-works/pi-durable": "1.4.0",
111
+ "@earendil-works/pi-mcp": "1.4.0",
112
+ "@earendil-works/pi-telemetry": "1.4.0",
113
+ "@earendil-works/pi-tui": "1.4.0",
113
114
  "@modelcontextprotocol/sdk": "^1.12.1",
114
115
  "proper-lockfile": "4.1.2",
115
116
  "smol-toml": "^1.8.0",
@@ -196,7 +197,7 @@
196
197
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
197
198
  "check:toolchain": "pnpm lint && pnpm typecheck",
198
199
  "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",
199
- "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-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-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-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",
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-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",
200
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",
201
202
  "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-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
202
203
  },
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # WHY THIS EXISTS (measured, #51 B/B2, 2026-07-22 — not inferred)
5
5
  #
6
- # Claude Code >= 2.1.217 supports the shell-less EXEC form: `command` is the
6
+ # Claude Code 2.1.217 and later support the shell-less EXEC form: `command` is the
7
7
  # executable and `args` is argv, each element substituted as a plain string, with
8
8
  # no shell anywhere on the path. Measured at 2.1.217: a hook declared that way
9
9
  # receives every `args` element verbatim (a literal `${HOME}` arrives LITERAL —
@@ -47,8 +47,8 @@ if [ "$#" -eq 0 ]; then
47
47
  still reports the hook as `exit_code: 0, outcome: success` (measured on 2.1.138),
48
48
  so nothing upstream will ever tell you.
49
49
 
50
- entwurf supports Claude Code >= 2.1.217 — the version whose runtime behavior was
51
- actually verified end to end. There is no shell-form fallback.
50
+ entwurf supports Claude Code >= 2.1.306 (the exec form was verified end to end at
51
+ 2.1.217; same-id resume needs the newer CLI). There is no shell-form fallback.
52
52
 
53
53
  Fix: update Claude Code, then re-run
54
54
  ./run.sh install-meta-bridge && ./run.sh doctor-meta-bridge
@@ -219,6 +219,7 @@ export class TaskBadge {
219
219
  ...tally("aborting", (node) => node.abortRequested),
220
220
  ...tally("completing", (node) => !node.abortRequested && node.state.status === "completing"),
221
221
  ...tally("waiting", (node) => !node.abortRequested && node.state.status === "waiting"),
222
+ ...tally("sleeping", (node) => !node.abortRequested && node.state.status === "sleeping"),
222
223
  ...tally("pending", (node) => !node.abortRequested && node.state.status === "pending"),
223
224
  ].join(", ");
224
225
  }
@@ -621,19 +622,33 @@ class DurableTui {
621
622
  }
622
623
  }
623
624
 
624
- function describeTask(node ) {
625
+ /** A task's line in the tasks panel: a sleeping task says when it wakes, a blocked one why it cannot run. */
626
+ export function describeTask(node ) {
625
627
  const state = node.state;
626
628
  const status =
627
629
  state.status === "waiting"
628
630
  ? `waiting on ${state.on.join(", ")}`
629
631
  : state.status === "completing"
630
632
  ? `completing (${state.outcome})`
631
- : `${state.status} ${state.phase}`;
632
- const flags = [node.background ? "background" : "", node.abortRequested ? "aborting" : ""].filter(Boolean);
633
+ : state.status === "sleeping"
634
+ ? `sleeping ${state.phase} until ${localTime(state.until)}`
635
+ : `${state.status} ${state.phase}`;
636
+ const flags = [
637
+ node.background ? "background" : "",
638
+ node.abortRequested ? "aborting" : "",
639
+ node.blocked !== undefined ? `blocked:${node.blocked}` : "",
640
+ ].filter(Boolean);
633
641
  const owned = node.conversations.length > 0 ? ` owns conversation ${node.conversations.join(", ")}` : "";
634
642
  return `${node.kind} #${node.id}: ${status}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${owned}`;
635
643
  }
636
644
 
645
+ /** Epoch milliseconds as local wall-clock time, `YYYY-MM-DD HH:MM:SS`. */
646
+ function localTime(epoch ) {
647
+ const at = new Date(epoch);
648
+ const pad = (n ) => String(n).padStart(2, "0");
649
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}`;
650
+ }
651
+
637
652
  function userText(content ) {
638
653
  if (typeof content === "string") return content;
639
654
  return content
@@ -76,8 +76,8 @@
76
76
  {
77
77
  "name": "tui.js",
78
78
  "source": "pi/pi-durable/src/experimental/durable/tui.ts",
79
- "blob": "eb5e2ec4e98919cd3ff4f81f166eb52afe6555af",
80
- "sha256": "a1b0d9d9358c5b54ac0974de84068b7792669171917ebd7b7351374ed625c9c5"
79
+ "blob": "405600f6ba1558bf6bf22d144afdcf3f1295c282",
80
+ "sha256": "d4bf4e2861bde917e1dcdea6f063bdc3e101ab500107a9c5fab0e3ce97fb2c4a"
81
81
  },
82
82
  {
83
83
  "name": "harness-setup.js",
@@ -132,17 +132,17 @@
132
132
  ]
133
133
  },
134
134
  "sdk": {
135
- "comment": "The published library set the carrier runs against, derived from the 1.3.0 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare. direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.3.0 pins). Each is an exact production dependency in package.json. Since 1.3.0 the members' own edges are exact too (upstream #10865), and the installed physical edges are still what must equal these versions.",
135
+ "comment": "The published library set the carrier runs against, derived from the 1.4.0 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare. direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.4.0 pins). Each is an exact production dependency in package.json. Since 1.3.0 the members' own edges are exact too (upstream #10865), and the installed physical edges are still what must equal these versions.",
136
136
  "members": {
137
- "@earendil-works/chord": "1.3.0",
138
- "@earendil-works/pi-agent-core": "1.3.0",
139
- "@earendil-works/pi-ai": "1.3.0",
140
- "@earendil-works/pi-codemode": "1.3.0",
141
- "@earendil-works/pi-coding-agent": "1.3.0",
142
- "@earendil-works/pi-durable": "1.3.0",
143
- "@earendil-works/pi-mcp": "1.3.0",
144
- "@earendil-works/pi-telemetry": "1.3.0",
145
- "@earendil-works/pi-tui": "1.3.0"
137
+ "@earendil-works/chord": "1.4.0",
138
+ "@earendil-works/pi-agent-core": "1.4.0",
139
+ "@earendil-works/pi-ai": "1.4.0",
140
+ "@earendil-works/pi-codemode": "1.4.0",
141
+ "@earendil-works/pi-coding-agent": "1.4.0",
142
+ "@earendil-works/pi-durable": "1.4.0",
143
+ "@earendil-works/pi-mcp": "1.4.0",
144
+ "@earendil-works/pi-telemetry": "1.4.0",
145
+ "@earendil-works/pi-tui": "1.4.0"
146
146
  },
147
147
  "direct": {
148
148
  "proper-lockfile": "4.1.2"
@@ -59,6 +59,7 @@
59
59
  * - { type: "get_info" }
60
60
  * - { type: "clear", summarize?: boolean }
61
61
  * - { type: "abort" }
62
+ * - { type: "retire", gardenId: "...", nativeSessionId: "..." } (internal, idle TUI only)
62
63
  * Responses are JSON objects with { type: "response", command, success, data?, error? }
63
64
  * (No event channel — the turn_end subscribe surface was removed with the
64
65
  * Send-is-throw cleanup; see note above.)
@@ -79,6 +80,16 @@ import {
79
80
  noteCompactionBefore,
80
81
  noteCompactionTerminal,
81
82
  } from "./lib/compaction-send-guard.js";
83
+ import {
84
+ acceptControlConnection,
85
+ type ControlBinding,
86
+ closeControlConnections,
87
+ endRetireResponse,
88
+ invalidateControlBinding,
89
+ requestControlRetire,
90
+ retireResponseEnvelope,
91
+ startOrRebindControlServer,
92
+ } from "./lib/control-retire.js";
82
93
  import { sendReceiptBoundary } from "./lib/control-send-receipt.js";
83
94
  import { CONTROL_SOCKET_SUFFIX, defaultControlSocketDir } from "./lib/control-socket-path.js";
84
95
  import {
@@ -121,6 +132,10 @@ interface SocketState {
121
132
  server: net.Server | null;
122
133
  socketPath: string | null;
123
134
  context: ExtensionContext | null;
135
+ binding: ControlBinding | null;
136
+ closing: boolean;
137
+ retireRequested: boolean;
138
+ connections: Set<net.Socket>;
124
139
  compaction: CompactionGuard;
125
140
  }
126
141
 
@@ -485,12 +500,27 @@ async function handleCommand(
485
500
  state: SocketState,
486
501
  command: RpcCommand,
487
502
  socket: net.Socket,
503
+ serverBinding: ControlBinding | null,
488
504
  ): Promise<void> {
489
505
  const id = "id" in command && typeof command.id === "string" ? command.id : undefined;
490
506
  const respond = (success: boolean, commandName: string, data?: unknown, error?: string) => {
491
507
  writeResponse(socket, { type: "response", command: commandName, success, data, error, id });
492
508
  };
493
509
 
510
+ // The public core checks caller/self, freshness and target admission, not an operator grant.
511
+ // Attribution is not authorization: an explicit operator request and agent operating policy
512
+ // are required; use host approval where provided/configured. No async gap exists between
513
+ // the receiver's final same-session checks and the SDK void call.
514
+ if (command.type === "retire") {
515
+ const result = requestControlRetire(state, serverBinding, residentGardenId, command);
516
+ endRetireResponse(socket, retireResponseEnvelope(result, id));
517
+ return;
518
+ }
519
+
520
+ if (state.closing || state.retireRequested) {
521
+ respond(false, command.type, undefined, "target-closing");
522
+ return;
523
+ }
494
524
  const ctx = state.context;
495
525
  if (!ctx) {
496
526
  respond(false, command.type, undefined, "Session not ready");
@@ -690,6 +720,7 @@ async function handleCommand(
690
720
 
691
721
  async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: string): Promise<net.Server> {
692
722
  const server = net.createServer((socket) => {
723
+ const serverBinding = acceptControlConnection(state, socket);
693
724
  // FIRST — before setEncoding, before the data handler, and before any response can be
694
725
  // written back. The policy itself (and why this process died without it) lives with the
695
726
  // rest of the wire protocol in lib/entwurf-control-rpc.ts.
@@ -729,7 +760,7 @@ async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: st
729
760
  // here would serialize subsequent commands on the same socket and
730
761
  // tangle teardown ordering during socket close.
731
762
  const commandName = parsed.command?.type ?? "unknown";
732
- void handleCommand(pi, state, parsed.command!, socket).catch((error) => {
763
+ void handleCommand(pi, state, parsed.command!, socket, serverBinding).catch((error) => {
733
764
  const message = error instanceof Error ? error.message : String(error);
734
765
  writeResponse(socket, {
735
766
  type: "response",
@@ -762,24 +793,21 @@ async function startControlServer(
762
793
  state: SocketState,
763
794
  ctx: ExtensionContext,
764
795
  socketPath: string,
796
+ gardenId: string,
765
797
  ): Promise<void> {
766
- await ensureControlDir();
767
- await gcStaleSockets();
768
-
769
- if (state.socketPath === socketPath && state.server) {
770
- state.context = ctx;
771
- return;
772
- }
773
-
774
- await stopControlServer(state);
775
- await removeSocket(socketPath);
776
-
777
- state.context = ctx;
778
- state.socketPath = socketPath;
779
- state.server = await createServer(pi, state, socketPath);
798
+ await startOrRebindControlServer(state, ctx, socketPath, gardenId, {
799
+ prepare: async () => {
800
+ await ensureControlDir();
801
+ await gcStaleSockets();
802
+ },
803
+ stop: () => stopControlServer(state),
804
+ remove: removeSocket,
805
+ create: (address) => createServer(pi, state, address),
806
+ });
780
807
  }
781
808
 
782
809
  async function stopControlServer(state: SocketState): Promise<void> {
810
+ invalidateControlBinding(state);
783
811
  if (!state.server) {
784
812
  await removeSocket(state.socketPath);
785
813
  state.socketPath = null;
@@ -788,7 +816,7 @@ async function stopControlServer(state: SocketState): Promise<void> {
788
816
 
789
817
  const socketPath = state.socketPath;
790
818
  state.socketPath = null;
791
- await new Promise<void>((resolve) => state.server?.close(() => resolve()));
819
+ await closeControlConnections(state.server, state.connections);
792
820
  state.server = null;
793
821
  await removeSocket(socketPath);
794
822
  }
@@ -958,6 +986,11 @@ export default function (pi: ExtensionAPI) {
958
986
  server: null,
959
987
  socketPath: null,
960
988
  context: null,
989
+ binding: null,
990
+ // Before session_start, commands refuse target-closing rather than Session not ready.
991
+ closing: true,
992
+ retireRequested: false,
993
+ connections: new Set(),
961
994
  compaction: createCompactionGuard(),
962
995
  };
963
996
 
@@ -1022,7 +1055,7 @@ export default function (pi: ExtensionAPI) {
1022
1055
  // naming the fresh-cut command, control server refused. There is no mixed
1023
1056
  // store to warn about; the store is either clean or this session has no
1024
1057
  // address.
1025
- await startControlServer(pi, state, ctx, birth.socketPath);
1058
+ await startControlServer(pi, state, ctx, birth.socketPath, birth.gardenId);
1026
1059
  // Identity before UI (#125): the carrier env and the MCP registration that carries the
1027
1060
  // born gardenId are both made before any status rendering, so a visual failure can never
1028
1061
  // leave a born citizen without its identity carrier or its verbs.
@@ -1082,10 +1115,12 @@ export default function (pi: ExtensionAPI) {
1082
1115
  // simply rebinds to the new address. There is no id to police at the pre-event.
1083
1116
 
1084
1117
  pi.on("session_shutdown", async () => {
1085
- noteCompactionTerminal(state.compaction);
1086
- updateStatus(state.context, false, null);
1087
- updateSessionEnv(state.context, false, null);
1118
+ const previousContext = state.context;
1119
+ invalidateControlBinding(state);
1088
1120
  residentGardenId = null;
1121
+ noteCompactionTerminal(state.compaction);
1122
+ updateStatus(previousContext, false, null);
1123
+ updateSessionEnv(previousContext, false, null);
1089
1124
  await stopControlServer(state);
1090
1125
  });
1091
1126
 
@@ -239,9 +239,11 @@ export interface AcpBackendAdapter {
239
239
 
240
240
  /** OPTIONAL capability (#127): recognise a typed vendor rejection of `session/prompt`
241
241
  * whose raw text must not become pi's `errorMessage`. pi decides a whole-prompt replay
242
- * from `errorMessage` alone, and a vendor's "Internal error" — or any digit run in the
243
- * ids it carries — reads as transient, so a turn whose tools already ran would be resent
244
- * from a cold session. A recognised rejection returns a fixed, authored verdict for
242
+ * from `errorMessage` alone, and a vendor's "Internal error" reads as transient
243
+ * (pi-ai's `internal.?error` term), so a turn whose tools already ran would be resent
244
+ * from a cold session. Through pi-ai 1.3 a digit run inside the ids it carries (`…8503…`)
245
+ * read as an HTTP status too; pi-ai 1.4 bounds the status terms (upstream #11057), so
246
+ * the ids no longer trigger it — the "Internal error" text alone still does. A recognised rejection returns a fixed, authored verdict for
245
247
  * `errorMessage`; backend.ts keeps the raw rejection as a pi diagnostic instead.
246
248
  *
247
249
  * ABSENT means every failure of this backend keeps the verbatim-first `errorMessage`.
@@ -668,13 +668,15 @@ async function settleChildEnd(latch: ChildEndLatch, ms: number): Promise<void> {
668
668
  * may appear here, or a dead child would put a full cold prompt replay back on
669
669
  * the table — with the tool side effects this turn already produced.
670
670
  *
671
- * That pattern is a bare substring alternation (`new RegExp(terms.join("|"), "i")`
672
- * — NO word boundaries) and its terms enumerate the HTTP statuses "429", "500",
673
- * "502", "503", "504" and "524". So the settle bound is deliberately NOT interpolated: rendering
674
- * CHILD_END_SETTLE_MS made this line say "within 500ms", which pi read as an
675
- * HTTP 500 and classified as transient. Naming the window in prose keeps a later
676
- * change of that constant from silently re-arming the replay — do not "improve"
677
- * this by putting the number back; the bound belongs in the code and the gate.
671
+ * That pattern is a substring alternation (`new RegExp(terms.join("|"), "i")` — NO word
672
+ * boundaries on its word terms). Through pi-ai 1.3 its HTTP status terms ("429", "500",
673
+ * "502", "503", "504", "520", "524") were bare substrings too, and rendering
674
+ * CHILD_END_SETTLE_MS made this line say "within 500ms", which pi read as an HTTP 500 and
675
+ * classified as transient. pi-ai 1.4 bounds only those status terms (upstream #11057), so
676
+ * "500ms" no longer matches; the word terms above are still unbounded. The settle bound
677
+ * stays deliberately NOT interpolated: naming the window in prose keeps a later change of
678
+ * that constant, or of the classifier, from silently re-arming the replay — do not
679
+ * "improve" this by putting the number back; the bound belongs in the code and the gate.
678
680
  *
679
681
  * The three endings are kept DISTINCT on purpose (issue #72 Done-when): an
680
682
  * exit code, a signal, and "we waited and it never said" are three different
@@ -1468,8 +1470,9 @@ export function streamAcpTurn(
1468
1470
  // the turn-scoped full-transcript-replay cause.
1469
1471
  const hint = aborted ? undefined : actionableAcpBackendHint(full);
1470
1472
  // #127: pi's retry and overflow classifiers read `errorMessage` ALONE, and a
1471
- // vendor's "Internal error" or any digit run in its ids/stderr reads as transient
1472
- // — a replay of a turn whose tools already ran. A rejection the adapter names gets
1473
+ // vendor's "Internal error" reads as transient — a replay of a turn whose tools
1474
+ // already ran. (Through pi-ai 1.3 a digit run in its ids/stderr did too; 1.4 bounds
1475
+ // the status terms, upstream #11057.) A rejection the adapter names gets
1473
1476
  // its fixed verdict there; every raw part rides one diagnostic, appended after any
1474
1477
  // existing ones. An abort keeps the abort path.
1475
1478
  const rejection = aborted ? undefined : adapter?.readPromptRejection?.(err);