agents-can-communicate 0.4.4 → 0.5.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 (55) hide show
  1. package/README.md +5 -3
  2. package/bin/entrypoints/acc-hook.mjs +58 -5
  3. package/docs/ARCHITECTURE.md +9 -4
  4. package/docs/CLI.md +12 -8
  5. package/docs/GETTING_STARTED.md +1 -1
  6. package/docs/PROTOCOL.md +7 -0
  7. package/docs/RELEASING.md +1 -1
  8. package/docs/SECURITY_MODEL.md +4 -2
  9. package/docs/TROUBLESHOOTING.md +9 -5
  10. package/docs/UPGRADING.md +86 -28
  11. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  12. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  13. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +4 -2
  14. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +4 -0
  15. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +67 -22
  16. package/node_modules/@agents-can-communicate/adapter-codex/src/permission-ownership.mjs +19 -2
  17. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  18. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  19. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  20. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  21. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +63 -16
  24. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +16 -1
  25. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  26. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +7 -1
  27. package/node_modules/@agents-can-communicate/cli/src/index.mjs +5 -0
  28. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +187 -11
  29. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/diagnostics.mjs +2 -1
  30. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +17 -2
  31. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +14 -6
  32. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +44 -25
  33. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +46 -29
  34. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance-state.mjs +1 -1
  35. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance-worker.mjs +9 -1
  36. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +52 -4
  37. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/management-recovery.mjs +18 -9
  38. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +125 -0
  39. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/retire.mjs +56 -0
  40. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/staging.mjs +95 -0
  41. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +8 -1
  42. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +18 -8
  43. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +83 -7
  44. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  45. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  46. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +44 -2
  47. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  48. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +10 -1
  49. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +55 -6
  50. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  51. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  52. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  54. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +38 -4
  55. package/package.json +2 -1
package/README.md CHANGED
@@ -80,7 +80,7 @@ state, commonly your home directory, cannot be used as a workspace; start the cl
80
80
  project directory. See [Troubleshooting](docs/TROUBLESHOOTING.md).
81
81
 
82
82
  Already using ACC? Follow the [upgrade guide](docs/UPGRADING.md), including the 0.4.x →
83
- 0.4.4 update and the data-format boundary when moving from 0.3.1.
83
+ 0.5.0 update and the data-format boundary when moving from 0.3.1.
84
84
 
85
85
  ## When messages arrive
86
86
 
@@ -141,8 +141,10 @@ One npm package. ACC needs no separate account, model API key, or hosted service
141
141
  ## Update or remove ACC
142
142
 
143
143
  Initial installation enables automatic updates. ACC downloads stable releases in the
144
- background and waits for active clients and ACC processes to exit before switching the
145
- runtime and refreshing integrations. When a verified Codex service needs a restart,
144
+ background, then switches the runtime and refreshes integrations once nothing blocks. A
145
+ live client or ACC process blocks only while the store contract it declares differs from
146
+ the incoming version's or is unknown; one declaring a matching contract keeps running
147
+ through the switch. When a verified Codex service needs a restart,
146
148
  `acc update` asks once, then completes maintenance in a separate process. Open clients
147
149
  disconnect; `acc doctor` reports progress and the result. See [update and recovery details](docs/UPGRADING.md#confirmed-client-service-maintenance).
148
150
  Restart or resume clients afterward and complete any requested hook or plugin trust review.
@@ -6,9 +6,11 @@
6
6
  // someone's session stops working. Unknown adapter, malformed payload, broken
7
7
  // store, missing binding - all of them end in "allow, exit 0".
8
8
  import { randomBytes } from "node:crypto";
9
+ import { pathToFileURL } from "node:url";
9
10
 
10
11
  import { createId } from "@agents-can-communicate/protocol";
11
12
  import { runHook } from "@agents-can-communicate/hook-runner";
13
+ import { hookEntrypointFor, resolvePinnedGeneration } from "@agents-can-communicate/cli";
12
14
 
13
15
  import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
14
16
  import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
@@ -38,15 +40,66 @@ const readStdin = () => new Promise(resolve => {
38
40
 
39
41
  import { completeHookOutput } from "./hook-output.mjs";
40
42
 
41
- export async function main() {
42
- const [adapterId] = process.argv.slice(2);
43
+ // A pin is keyed by harness session id, which only an adapter's own
44
+ // normalisation can find inside a payload. This runs the same normalisation
45
+ // `runHook` will run a moment later - cheap, in-memory, no store touched - so
46
+ // a hook that has no pin to look up pays nothing beyond it. Any adapter or
47
+ // payload that will not yield a session id simply has no pin to find, same as
48
+ // today's no-pin behaviour.
49
+ async function harnessSessionIdFor(adapterId, payload) {
50
+ try {
51
+ const event = await adapters[adapterId]?.normalizeHook(payload);
52
+ return typeof event?.sessionId === "string" ? event.sessionId : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
43
57
 
44
- let payload = null;
58
+ // A pinned generation is only ever another build of this same codebase, but
59
+ // it can still be broken on disk - deleted mid-write, a corrupt install. This
60
+ // tries it and reports failure rather than letting an import error or a
61
+ // throwing `main` escape: the caller then runs the turn on the active
62
+ // generation instead, exactly as if there had been no pin.
63
+ //
64
+ // `delegated` is the circuit breaker on chaining: a hook already running
65
+ // because it was delegated to never looks for a pin of its own. Ordinary
66
+ // resolution already stops itself after one hop - the generation a pin names
67
+ // compares its own pin against the exact root it was just handed, so they
68
+ // match and it returns null - but that self-termination leans on a single
69
+ // pin record staying the value both reads saw. `delegated` does not: it
70
+ // forbids a second hop outright, independent of anything a pin (rewritten
71
+ // mid-resolution by another hook for the same session, or simply wrong)
72
+ // could say. Unbounded delegation would be a hang, worse than any of the
73
+ // failures this file already falls open from.
74
+ async function delegateToPin({ managerRoot, packageRoot, adapterId, payload, delegated }) {
75
+ if (delegated || managerRoot === null || packageRoot === null) return false;
76
+ const harnessSessionId = await harnessSessionIdFor(adapterId, payload);
77
+ if (harnessSessionId === null) return false;
78
+ const pinned = await resolvePinnedGeneration({ root: managerRoot, harnessSessionId, active: packageRoot });
79
+ if (pinned === null) return false;
45
80
  try {
46
- payload = JSON.parse(await readStdin());
81
+ const runtime = await import(pathToFileURL(hookEntrypointFor(pinned)).href);
82
+ await runtime.main({ managerRoot, packageRoot: pinned, payload, delegated: true });
83
+ return true;
47
84
  } catch {
48
- payload = null;
85
+ return false;
49
86
  }
87
+ }
88
+
89
+ export async function main({ managerRoot = null, packageRoot = null, payload, delegated = false } = {}) {
90
+ const [adapterId] = process.argv.slice(2);
91
+
92
+ // A delegating caller already read stdin once for the whole process and
93
+ // hands its parsed payload down; only the outermost call reads it here.
94
+ if (payload === undefined) {
95
+ try {
96
+ payload = JSON.parse(await readStdin());
97
+ } catch {
98
+ payload = null;
99
+ }
100
+ }
101
+
102
+ if (await delegateToPin({ managerRoot, packageRoot, adapterId, payload, delegated })) return;
50
103
 
51
104
  const result = await runHook({ adapterId, payload, adapters,
52
105
  runtime: { clock: { now: () => new Date().toISOString() },
@@ -174,14 +174,19 @@ The CLI owns installation generations under `<dataHome>/acc/runtime`, outside wo
174
174
  All five launch paths select a generation and publish an actual-process lease under one
175
175
  admission mutex before loading workspace-capable code. Immutable launcher modules and
176
176
  runtime directories preserve in-progress imports. Leases survive `main()` returning and
177
- are removed only after confirmed process death. Parsed help, version, and update recovery
178
- remain available when workspace admission is unavailable.
177
+ are removed only after confirmed process death. A session is also pinned to the generation
178
+ it started with: its hooks delegate to that generation while the pin declares the same
179
+ store contract this one does, and run the active generation otherwise. A generation
180
+ directory is reclaimed once no control pointer, live lease, live pin or staging hold names
181
+ it. Parsed help, version, and update recovery remain available when workspace admission is
182
+ unavailable.
179
183
 
180
184
  An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
181
185
  stable package identity and the discovered integrity, and health-checks the staged runtime.
182
186
  Installer detection and planning happen before exclusive admission. The worker then checks
183
- ACC process leases and native binding PIDs, records `activating`, applies every integration,
184
- and only publishes the new active pointer after all applications succeed. Partial refresh
187
+ ACC process leases and native binding PIDs and waits only for those whose declared store
188
+ contract differs from the incoming generation's or is absent, records `activating`, applies
189
+ every integration, and only publishes the new active pointer after all applications succeed. Partial refresh
185
190
  remains fenced and repairs forward, with the old active pointer retained but normal
186
191
  workspace admission unavailable until recovery completes. Native bindings cease holding
187
192
  after observed SessionEnd cleanup or confirmed process death; the vendor daemon may remain
package/docs/CLI.md CHANGED
@@ -274,17 +274,21 @@ An initial `acc install` enables automatic updates. Reinstalling preserves an ex
274
274
  `acc update --auto off` choice. A full uninstall pauses updates; reinstalling restores
275
275
  the remembered setting. A background worker checks stable releases
276
276
  at most once a day, downloads and verifies a separate runtime, then refreshes installed
277
- integrations and skills after ACC process leases and native binding holds clear. ACC leases
278
- require confirmed process exit; a native binding can clear on observed SessionEnd even
279
- while its vendor daemon remains alive, or on confirmed process death. Unknown PIDs remain
280
- holds. `finish`, presence TTL, and delivery off are not observed native end. Hooks do not
277
+ integrations and skills once no hold blocks. A runtime lease or native binding blocks only
278
+ while the store contract it declares differs from the incoming version's, or while it
279
+ declares none; records written before 0.5.0 declare none, so the first update after
280
+ upgrading still waits for them. ACC leases end on confirmed process exit; a native binding
281
+ can clear on observed SessionEnd even while its vendor daemon remains alive, or on
282
+ confirmed process death. Unknown PIDs remain holds. `finish`, presence TTL, and delivery off are not observed native end. Hooks do not
281
283
  wait for network work. `ACC_NO_UPDATE_CHECK=1` disables update networking and background
282
284
  scheduling.
283
285
 
284
- `acc update` requests the update immediately. If a verified Codex service blocks activation
285
- or runs an older version than the installed CLI, it asks once to restart that service.
286
- After confirmation, a detached worker waits for idle work and other ACC processes, refreshes
287
- integrations, restarts the service and verifies the result. Open clients disconnect; use
286
+ `acc update` requests the update immediately. If a verified Codex service holds a native
287
+ binding that blocks activation, or serves a version its captured native-delivery contract
288
+ refuses, it asks once to restart that service.
289
+ After confirmation, a detached worker waits for idle work and for any ACC process whose
290
+ declared store contract still holds the update, refreshes integrations, restarts the
291
+ service and verifies the result. Open clients disconnect; use
288
292
  `acc doctor` for progress and resume the clients afterward. `--yes` supplies explicit
289
293
  noninteractive restart consent. Without consent, or with unknown ownership, the update
290
294
  remains pending. See [maintenance limits and recovery](UPGRADING.md#confirmed-client-service-maintenance). `--check` only checks and cannot be combined with
@@ -15,7 +15,7 @@ pair or session-bound ACC MCP tools. See [CLI ownership](CLI.md#coordinate-from-
15
15
 
16
16
  ACC requires macOS or Linux and Node.js 24 or newer.
17
17
 
18
- Already using ACC? Follow the [upgrade guide](UPGRADING.md) for the 0.4.x → 0.4.4
18
+ Already using ACC? Follow the [upgrade guide](UPGRADING.md) for the 0.4.x → 0.5.0
19
19
  update or the data-format boundary when upgrading from 0.3.1.
20
20
 
21
21
  ```bash
package/docs/PROTOCOL.md CHANGED
@@ -238,6 +238,13 @@ sessionId · generation · adapterId · clientVersion · availableModes
238
238
  livePolicy · opaqueEndpointRef · leaseUntil
239
239
  ```
240
240
 
241
+ `clientVersion` is the version that answered the handshake and was admitted by the
242
+ adapter's captured contract — the process that will serve this binding's live pushes,
243
+ which is not always the version the client's own executable reports. A background
244
+ service that updated under its CLI serves the updated build, and the binding names that
245
+ build. The version the executable reported is recorded separately, beside the session's
246
+ owner record.
247
+
241
248
  The recipient owns `livePolicy` because native push may start a model turn:
242
249
 
243
250
  - `off`: inbox and normal next-turn paths only;
package/docs/RELEASING.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Releasing
2
2
 
3
3
  Use this procedure to build one auditable npm artifact, verify that exact tarball, and keep
4
- its evidence tied to the commit that supplied its bytes. The package is currently `0.4.4`;
4
+ its evidence tied to the commit that supplied its bytes. The package is currently `0.5.0`;
5
5
  the commands derive the version from `package.json` so the filename cannot drift.
6
6
 
7
7
  ```mermaid
@@ -121,8 +121,10 @@ sha512 integrity against registry metadata, installs with lifecycle scripts disa
121
121
  validates the staged runtime before activation. These checks retain the registry and package
122
122
  author as a software-supply trust boundary; they do not establish that package code is safe.
123
123
 
124
- ACC process leases hold activation until confirmed exit. Native bindings hold until observed
125
- SessionEnd cleanup or confirmed process death; unknown PIDs remain conservative holds.
124
+ ACC process leases and native bindings record the store contract they speak, and hold
125
+ activation while that contract differs from the incoming version's or is absent. A lease
126
+ ends on confirmed exit, a native binding on observed SessionEnd cleanup or confirmed process
127
+ death; unknown PIDs and undeclared contracts remain conservative holds.
126
128
  `acc finish`, presence TTL, and delivery off do not establish native lifecycle end.
127
129
  Normal installation and message delivery do not manage vendor services. Explicit
128
130
  `acc update` maintenance requires one separate confirmation (or `--yes`) for the recorded
@@ -236,11 +236,15 @@ Next: [Getting started](GETTING_STARTED.md) · [Capabilities](CAPABILITIES.md)
236
236
 
237
237
  ## An automatic update is pending
238
238
 
239
- Run `acc doctor` to see the update policy and pending notice. ACC process leases, including
240
- persistent MCP servers, require confirmed process exit. Native bindings clear on observed
241
- SessionEnd or confirmed process death; a vendor daemon may remain alive after SessionEnd.
242
- Unknown PIDs remain holds until lifecycle cleanup. Close the relevant client sessions and
243
- ACC processes; `finish`, presence TTL, and delivery off do not prove native end.
239
+ Run `acc doctor` to see the update policy and pending notice. A hold keeps an update
240
+ pending only while the store contract it declares differs from the incoming version's or
241
+ is unknown; records written before 0.5.0 declare none, so the first update after upgrading
242
+ still waits for all of them. `acc update` names each remaining hold with its process and
243
+ its declared contract. ACC process leases, including persistent MCP servers, end on
244
+ confirmed process exit. Native bindings clear on observed SessionEnd or confirmed process
245
+ death; a vendor daemon may remain alive after SessionEnd. Unknown PIDs remain holds until
246
+ lifecycle cleanup. Close the client sessions and ACC processes the notice names;
247
+ `finish`, presence TTL, and delivery off do not prove native end.
244
248
  `acc update` can offer a confirmed restart of an eligible Codex service. Unknown or
245
249
  unrelated holds still require lifecycle cleanup or confirmed process exit; safety holds
246
250
  do not expire merely by elapsed time. See
package/docs/UPGRADING.md CHANGED
@@ -1,14 +1,14 @@
1
- # Upgrading to 0.4.4
1
+ # Upgrading to 0.5.0
2
2
 
3
- ## From 0.4.3, 0.4.2, 0.4.1 or 0.4.0
3
+ ## From 0.4.4, 0.4.3, 0.4.2, 0.4.1 or 0.4.0
4
4
 
5
- This patch keeps the existing workspace data format. It adds confirmed service maintenance
6
- to the updater, preserves native delivery setup when readiness probes are unavailable,
7
- and names the actual processes blocking activation. It includes the previous 0.4.x
8
- outgoing sandbox permissions, delivery diagnostics, session recovery and integration fixes.
9
-
10
- Run the update command; it can offer a confirmed restart for an eligible Codex service.
11
- Other active clients and persistent ACC MCP processes still need to finish before activation:
5
+ This release keeps the existing workspace data format. It changes when an update is
6
+ allowed to take effect: activation is gated on the store contract a running process
7
+ declares rather than on which version it happens to be, sessions finish on the generation
8
+ they started with, unreferenced generation directories are reclaimed, and `acc uninstall`
9
+ retires the holds its own install published. It carries the previous 0.4.x confirmed
10
+ service maintenance, outgoing sandbox permissions, delivery diagnostics, session recovery
11
+ and integration fixes.
12
12
 
13
13
  ```bash
14
14
  acc update
@@ -16,8 +16,8 @@ acc version
16
16
  acc doctor
17
17
  ```
18
18
 
19
- After publication, `acc version` should report 0.4.4. If a pin keeps an older version,
20
- select 0.4.4 or clear it first. A managed update refreshes installed integrations and
19
+ After publication, `acc version` should report 0.5.0. If a pin keeps an older version,
20
+ select 0.5.0 or clear it first. A managed update refreshes installed integrations and
21
21
  skills; use the npm instructions below for unmanaged or pre-0.4 installations. Start
22
22
  clients again and complete any hook/trust review they request. With existing live consent,
23
23
  Codex 0.153.4 or newer on macOS arm64 receives local socket permissions when its workspace
@@ -44,16 +44,70 @@ In Grok, public status supplies the session's own CLI arguments through the next
44
44
  hook. Grok still uses explicit inbox reads; this patch adds no external wake or guards.
45
45
  Start clients in a project directory, rather than a home directory containing ACC state.
46
46
 
47
+ ## What still waits for a process to exit, and what no longer does
48
+
49
+ Earlier releases held a verified update until every live ACC process and every native
50
+ client binding was gone. Each of those holds now states the store contract it speaks, and
51
+ only two kinds keep an update pending:
52
+
53
+ - A hold whose declared store contract differs from the incoming version's. This is the
54
+ case the gate exists for, and a release that changes the store contract still needs
55
+ every live process to exit.
56
+ - A hold that declares no contract, which includes every record written by a release
57
+ before this one. Unknown cannot be compared, so it stays a conservative wait.
58
+
59
+ A hold declaring the same store contract as the incoming version proceeds. Several open
60
+ Claude Code sessions, a Codex daemon and an idle ACC MCP server no longer have to be
61
+ closed together to move between releases that share a contract.
62
+
63
+ The first update after installing this release still waits for every process, because the
64
+ holds it has to judge were written before the contract field existed. Close the relevant
65
+ clients and persistent ACC processes once, or accept the eligible Codex service
66
+ maintenance offer described below. Updates after that do not need it.
67
+
68
+ `acc update` names each remaining hold with its process and its declared contract, so a
69
+ wait now states its reason rather than only the PID. `acc doctor` reports the same
70
+ pending notice.
71
+
72
+ A session that is open while an activation completes keeps running the generation it
73
+ started with, including its hooks, until it ends; it does not get half of one version and
74
+ half of another. A session whose pin names a generation with a different store contract
75
+ runs the active generation instead, which is the same behaviour as having no pin.
76
+
77
+ A restart offer for a client's background service is now made only when the version that
78
+ service is actually running fails that client's captured contract, or when it still holds
79
+ a native binding. A Codex CLI that updated while its daemon kept serving the previous
80
+ build is an ordinary state, and it no longer produces a prompt to disconnect open clients.
81
+
82
+ Generation directories that nothing references are removed after an activation instead of
83
+ accumulating. A directory is kept while any control pointer, live runtime lease, live
84
+ session pin or in-flight staging hold names it, and an unreadable holder postpones the
85
+ whole sweep rather than being assumed dead.
86
+
87
+ `acc uninstall` retires the binding records and session pins its own install published,
88
+ so they stop holding a later activation, when the removal leaves no client installed. It
89
+ never signals or terminates a client process. Removing one client while another stays
90
+ installed retires nothing, because a binding record names the generation that published
91
+ it and no client id. Runtime leases are left in place on purpose: a lease is also what
92
+ protects the generation a running process is executing from, and clearing it would let
93
+ that directory be reclaimed out from under the process. A live ACC process therefore
94
+ keeps holding activation across an uninstall until it exits.
95
+
47
96
  ## Confirmed client service maintenance
48
97
 
49
- On macOS arm64, `acc update` can ask once to restart a verified Codex service that blocks
50
- activation or runs a different version from the installed CLI. The captured maintenance
51
- contract uses Codex CLI 0.154.0 and its `pid` backend, with compatible commands and protocol
98
+ On macOS arm64, `acc update` can ask once to restart a verified Codex service that holds a
99
+ native binding blocking activation, or that is serving a version outside the captured
100
+ native-delivery contract. A serving version that satisfies that contract is left alone,
101
+ even when the installed CLI has moved to a newer build. The captured maintenance contract
102
+ uses Codex CLI 0.154.0 and its `pid` backend, with compatible commands and protocol
52
103
  checked again at runtime. The installed CLI and managed executable must already match;
53
104
  ACC does not replace vendor binaries or install an operating-system service.
54
105
 
55
106
  After confirmation, a detached ACC worker waits up to 15 minutes for idle Codex turns,
56
- empty queues and other ACC processes to exit. It rechecks the approved candidate, settings,
107
+ empty queues, the requesting command to exit, and any other ACC process still holding the
108
+ update. It judges those holds by the same contract rule the activation path uses, so a
109
+ process declaring the incoming version's store contract does not delay the restart, and
110
+ one declaring a differing or unknown contract does. It rechecks the approved candidate, settings,
57
111
  PID/start time, executable, socket ownership and versions before stopping the service.
58
112
  It refreshes integrations, starts the service again and verifies the result. If refreshing
59
113
  integrations fails, it still restores the service and reports the incomplete update.
@@ -78,7 +132,7 @@ an update even when a native service or feature probe is temporarily unavailable
78
132
 
79
133
  An old installation can already be stuck with both a pre-0.4.4 pending runtime and the
80
134
  old updater that waits indefinitely. That code cannot discover this fix. Bootstrap the
81
- new launcher once with `npm install --global agents-can-communicate@0.4.4`, then run
135
+ new launcher once with `npm install --global agents-can-communicate@0.5.0`, then run
82
136
  `acc update`. The new launcher stages its verified runtime and retires only an exactly
83
137
  identified obsolete ACC background updater while no integrations are being written.
84
138
  This does not terminate an unrelated client or bypass its lifetime hold.
@@ -104,7 +158,7 @@ restores access; deleting or editing the record is not a remedy.
104
158
  2. Install the released package, then refresh the client integrations:
105
159
 
106
160
  ```bash
107
- npm install --global agents-can-communicate@0.4.4
161
+ npm install --global agents-can-communicate@0.5.0
108
162
  acc version
109
163
  acc install
110
164
  ```
@@ -139,13 +193,15 @@ an independent background process, normally at most once a day, downloads and ve
139
193
  separate runtime copy, and refreshes the installed integrations and skills before switching.
140
194
  The global npm package provides a launcher; the active runtime lives under ACC's data home.
141
195
 
142
- Running ACC processes, including idle MCP servers, hold the current version until
143
- confirmed process exit. Native bindings hold until observed client SessionEnd cleanup or
144
- confirmed process death; the vendor daemon can remain running after that lifecycle event.
145
- Unknown PIDs remain conservative holds until lifecycle cleanup. `acc finish`, presence TTL,
146
- and delivery off alone do not prove native end. Close the relevant client sessions and
147
- persistent ACC processes to release holds, or accept the eligible Codex service maintenance
148
- offer in `acc update`. Background updates never request fresh restart consent.
196
+ Running ACC processes, including idle MCP servers, still publish a hold for as long as they
197
+ live, and native bindings still hold until observed client SessionEnd cleanup or confirmed
198
+ process death; the vendor daemon can remain running after that lifecycle event. What
199
+ changed is which of those holds keeps an update pending: only one whose declared store
200
+ contract differs from the incoming version's, or that declares none at all. Unknown PIDs
201
+ remain conservative holds until lifecycle cleanup. `acc finish`, presence TTL, and delivery
202
+ off alone still do not prove native end. When a hold does block, close the relevant client
203
+ sessions and persistent ACC processes to release it, or accept the eligible Codex service
204
+ maintenance offer in `acc update`. Background updates never request fresh restart consent.
149
205
  Hooks never wait for a network download. If integration refresh temporarily prevents
150
206
  coordination, a hook lets the client continue; its next genuine user turn can restore a
151
207
  missing binding.
@@ -155,7 +211,7 @@ acc update # download and apply, or report what is keeping it
155
211
  acc update --check # check without installing or changing update settings
156
212
  acc update --auto off # disable background updates
157
213
  acc update --auto on # enable them again
158
- acc update --pin 0.4.4 # stay on this exact stable version
214
+ acc update --pin 0.5.0 # stay on this exact stable version
159
215
  acc update --pin none # follow stable releases again
160
216
  ```
161
217
 
@@ -173,9 +229,11 @@ lifecycle scripts. `acc doctor` reports the automatic-update policy and pending
173
229
  A failed download or verification keeps the active runtime. An interrupted integration
174
230
  refresh keeps workspace admission closed until `acc update` finishes the refresh. It does
175
231
  not roll workspace history back. A failed refresh exits with code `4` and reports the adapter
176
- and configuration problem to fix before retrying; a verified update waiting for live clients
177
- remains pending. Previous runtime and plugin cache versions are
178
- retained so existing launch paths survive the change.
232
+ and configuration problem to fix before retrying; a verified update waiting for a blocking
233
+ hold remains pending. A previous runtime version is retained for as long as anything can
234
+ still reach it — a control pointer, a live ACC process, or an open session pinned to it —
235
+ so existing launch paths survive the change; once nothing references it, its directory is
236
+ reclaimed after the next activation instead of accumulating.
179
237
 
180
238
  Client trust is still controlled by the client. Follow any reported activation or hook
181
239
  review steps; copied files do not prove the client has activated them. The initial
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.4.4",
3
+ "version": "0.5.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.4.4",
3
+ "version": "0.5.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -5,15 +5,17 @@ import certification from "../certification.json" with { type: "json" };
5
5
  import { createCodexMaintenance } from "./maintenance.mjs";
6
6
  export { sameMaintenanceIdentity } from "./maintenance.mjs";
7
7
 
8
- import { PROTOCOL_CONTRACT } from "./app-server-client.mjs";
8
+ import { CODEX_QUEUE_MINIMUM, PROTOCOL_CONTRACT } from "./app-server-client.mjs";
9
9
  import { allowOutcome, denyOutcome, injectOutcome, normalizeCodexHook }
10
10
  from "./hooks.mjs";
11
11
  import { planCodexInstall, detectCodex, installCodexPlugin, preflightCodexUninstall,
12
12
  uninstallCodexPlugin } from "./install.mjs";
13
13
  // Public native wiring is enabled only with installed-product capture evidence.
14
14
 
15
+ // Re-exported so existing importers of CODEX_QUEUE_MINIMUM keep resolving; the
16
+ // value now lives in app-server-client.mjs alongside the other version rules.
17
+ export { CODEX_QUEUE_MINIMUM };
15
18
  export const CODEX_VERSION = "0.147.0";
16
- export const CODEX_QUEUE_MINIMUM = "0.152.1";
17
19
  export const CODEX_DELIVERY_FALLBACK = Object.freeze({
18
20
  diagnostic: "Codex native delivery requires codex-cli 0.152.1 or newer on darwin-arm64, "
19
21
  + "recorded recipient consent and a reachable LocalDaemon session with exact thread, cwd, "
@@ -12,6 +12,10 @@ import { openWebSocketPeer } from "./ws-json-rpc.mjs";
12
12
 
13
13
  export const PROTOCOL_CONTRACT = "codex-app-server-thread-queue-v1";
14
14
  export const MINIMUM_VERSION = "0.152.1";
15
+ // The captured native-delivery contract's floor. Shared with MINIMUM_VERSION
16
+ // today, but named separately: this is the version a binding was verified
17
+ // against, not the probe's own support floor.
18
+ export const CODEX_QUEUE_MINIMUM = MINIMUM_VERSION;
15
19
  export const QUEUE_MODES = Object.freeze(["livePush", "idleWake", "busyQueue"]);
16
20
  const CLIENT_INFO = Object.freeze({ name: "agents-can-communicate", version: "0.2.0" });
17
21
  const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
@@ -1,9 +1,10 @@
1
1
  import { realpath } from "node:fs/promises";
2
2
  import { decisionBody } from "@agents-can-communicate/adapter-sdk";
3
3
 
4
- import { MINIMUM_VERSION, PROTOCOL_CONTRACT, QUEUE_MODES, addCodexQueueMessage,
5
- canonicalCwd, controlSocketPath, locateCodexThread, openCodexAppServer,
6
- parseStableVersion, probeCodexQueue, safeReason, serverVersionOf } from "./app-server-client.mjs";
4
+ import { CODEX_QUEUE_MINIMUM, MINIMUM_VERSION, PROTOCOL_CONTRACT, QUEUE_MODES,
5
+ addCodexQueueMessage, canonicalCwd, compareStableVersions, controlSocketPath,
6
+ locateCodexThread, openCodexAppServer, parseStableVersion, probeCodexQueue,
7
+ safeReason, serverVersionOf } from "./app-server-client.mjs";
7
8
  import { newEndpointId, readNativeEndpoint, removeNativeEndpoint, socketIsReady,
8
9
  writeNativeEndpoint } from "./native-endpoint.mjs";
9
10
 
@@ -62,13 +63,35 @@ export function planNativeActivation({ detection }) {
62
63
  ] };
63
64
  }
64
65
 
65
- async function verifyReceiver(peer, endpoint) {
66
- const probe = await probeCodexQueue(peer, { threadId: endpoint.threadId });
67
- if (!probe.supported) return probe.reasonCode === "probe_timeout" ? "handshake_timeout" : "protocol_mismatch";
68
- if (probe.serverVersion !== endpoint.clientVersion) return "handshake_version_mismatch";
69
- const located = await locateCodexThread(peer, { threadId: endpoint.threadId, cwd: endpoint.cwd });
70
- return located.found ? null : located.reasonCode === "cwd_mismatch"
71
- ? "workspace_identity_unavailable" : "handshake_failed";
66
+ // A daemon that restarted onto a newer build still satisfies the captured
67
+ // contract. The thread it may have lost is reported separately by
68
+ // locateCodexThread, so refusing on the version alone only hides the real
69
+ // reason. Returns the version that actually answered as `servingVersion`
70
+ // (null when refused) instead of mutating the endpoint it was handed, so a
71
+ // caller can decide what to persist on the one path that owns persistence,
72
+ // after its own validation has already run.
73
+ export async function verifyReceiver(peer, endpoint, { probe = probeCodexQueue,
74
+ locate = locateCodexThread } = {}) {
75
+ const result = await probe(peer, { threadId: endpoint.threadId });
76
+ if (!result.supported) {
77
+ // probeCodexQueue applies the same floor and answers first, so a daemon
78
+ // serving below the minimum arrives here as a refused probe rather than
79
+ // reaching the explicit check below. Reporting that as a protocol
80
+ // mismatch named the wrong condition everywhere it surfaced - doctor,
81
+ // the offer's safe error code, the recorded failure - and left the
82
+ // version answer this function already knows how to give unreachable.
83
+ return { reasonCode: result.reasonCode === "probe_timeout" ? "handshake_timeout"
84
+ : result.reasonCode === "below_minimum_version" ? "handshake_version_mismatch"
85
+ : "protocol_mismatch", servingVersion: null };
86
+ }
87
+ if (compareStableVersions(result.serverVersion, CODEX_QUEUE_MINIMUM) < 0) {
88
+ return { reasonCode: "handshake_version_mismatch", servingVersion: null };
89
+ }
90
+ const located = await locate(peer, { threadId: endpoint.threadId, cwd: endpoint.cwd });
91
+ return located.found
92
+ ? { reasonCode: null, servingVersion: result.serverVersion }
93
+ : { reasonCode: located.reasonCode === "cwd_mismatch" ? "workspace_identity_unavailable" : "handshake_failed",
94
+ servingVersion: null };
72
95
  }
73
96
 
74
97
  export async function bindNativeSession({ event, clientPid, clientVersion, runtimeDir,
@@ -86,20 +109,41 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
86
109
  const endpoint = { schemaVersion: 1, endpointId: newEndpointId(), socketPath,
87
110
  threadId: event.sessionId, cwd, clientVersion, protocolContract: PROTOCOL_CONTRACT,
88
111
  leaseUntil: new Date(now() + LEASE_MS).toISOString() };
89
- const reason = await verifyReceiver(peer, endpoint);
90
- if (reason !== null) return rejected(reason);
91
- await writeNativeEndpoint({ runtimeDir, record: endpoint });
92
- return handshake(endpoint, now);
112
+ const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
113
+ if (reasonCode !== null) return rejected(reasonCode);
114
+ // The daemon may have restarted onto a different build since the
115
+ // caller's claimed clientVersion was captured; persist what actually
116
+ // answered so the binding and its later reads reflect the serving
117
+ // process, not the caller's claim.
118
+ const verified = { ...endpoint, clientVersion: servingVersion };
119
+ await writeNativeEndpoint({ runtimeDir, record: verified });
120
+ return handshake(verified, now);
93
121
  });
94
122
  } catch (error) {
95
123
  return rejected(error?.code === "ETIMEDOUT" ? "handshake_timeout" : "handshake_failed");
96
124
  }
97
125
  }
98
126
 
127
+ // Resolving the binding's opaque reference is the whole of the identity check:
128
+ // the endpoint id is 128 random bits minted per bind, readNativeEndpoint
129
+ // re-checks that the record it read carries that exact id, and no two bindings
130
+ // can name the same record.
131
+ //
132
+ // The two records' clientVersion snapshots used to be compared here as well.
133
+ // That was never identity - the id already settles that - it was a third
134
+ // version rule, and the only one applied to a stored snapshot rather than to
135
+ // the process now answering. It is deliberately gone. It cannot refuse
136
+ // anything verifyReceiver does not refuse better two lines on, against the
137
+ // version that actually answers, with the thread and cwd, before anything is
138
+ // queued; and against the snapshot it could only ever be vacuous, because a
139
+ // record is written solely from a serving version that already cleared
140
+ // CODEX_QUEUE_MINIMUM. What it did do was fire on the case 0.5.0 exists to
141
+ // support: the endpoint records what the daemon serves, the binding recorded
142
+ // what `codex --version` printed, and a daemon that had updated under its CLI
143
+ // was refused here on that difference alone.
99
144
  async function receiverFor(binding, runtimeDir) {
100
145
  const endpoint = await readNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
101
- return endpoint?.clientVersion === binding?.clientVersion
102
- && await socketIsReady(endpoint?.socketPath) ? endpoint : null;
146
+ return endpoint !== null && await socketIsReady(endpoint.socketPath) ? endpoint : null;
103
147
  }
104
148
 
105
149
  export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 750,
@@ -109,8 +153,9 @@ export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 75
109
153
  if (endpoint === null) return rejected("handshake_failed");
110
154
  try {
111
155
  return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
112
- const reason = await verifyReceiver(peer, endpoint);
113
- return reason === null ? handshake(endpoint, now) : rejected(reason);
156
+ const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
157
+ return reasonCode === null
158
+ ? handshake({ ...endpoint, clientVersion: servingVersion }, now) : rejected(reasonCode);
114
159
  });
115
160
  } catch (error) {
116
161
  return rejected(error?.code === "ETIMEDOUT" ? "handshake_timeout" : "handshake_failed");
@@ -125,12 +170,12 @@ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 5
125
170
  if (endpoint === null) return rejected("recipient_unavailable");
126
171
  try {
127
172
  return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
128
- const reason = await verifyReceiver(peer, endpoint);
129
- if (reason !== null) return rejected(reason === "handshake_timeout" ? "transport_error"
130
- : reason === "handshake_version_mismatch" ? "unsupported_client_version" : "recipient_unavailable");
173
+ const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
174
+ if (reasonCode !== null) return rejected(reasonCode === "handshake_timeout" ? "transport_error"
175
+ : reasonCode === "handshake_version_mismatch" ? "unsupported_client_version" : "recipient_unavailable");
131
176
  await addCodexQueueMessage(peer, { threadId: endpoint.threadId,
132
177
  messageId: message.messageId, text: renderText(message) });
133
- return { accepted: true, transport: "codex-app-server", clientVersion: endpoint.clientVersion };
178
+ return { accepted: true, transport: "codex-app-server", clientVersion: servingVersion };
134
179
  });
135
180
  } catch (error) {
136
181
  if (["EPERM", "EACCES"].includes(error?.code)) return rejected("transport_permission_denied");