agents-can-communicate 0.8.3 → 0.8.5

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 (58) hide show
  1. package/bin/acc-update-worker.mjs +4 -1
  2. package/bin/entrypoints/acc-mcp.mjs +3 -0
  3. package/bin/entrypoints/acc.mjs +5 -0
  4. package/docs/ADAPTER_AUTHORING.md +3 -0
  5. package/docs/ARCHITECTURE.md +7 -2
  6. package/docs/CLI.md +7 -1
  7. package/docs/CONCEPTS.md +2 -2
  8. package/docs/CONFIGURATION.md +3 -2
  9. package/docs/GETTING_STARTED.md +12 -9
  10. package/docs/HOW_IT_WORKS.md +2 -1
  11. package/docs/TROUBLESHOOTING.md +18 -2
  12. package/docs/UPGRADING.md +35 -0
  13. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  14. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  15. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  16. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  17. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +7 -1
  18. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +24 -1
  19. package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +108 -0
  20. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +16 -6
  21. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +2 -0
  22. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +33 -9
  23. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +61 -2
  24. package/node_modules/@agents-can-communicate/adapter-codex/src/native-discovery.mjs +82 -0
  25. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +59 -5
  26. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  27. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  28. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  29. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  30. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  31. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
  32. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +6 -2
  33. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +17 -7
  34. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +12 -2
  35. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  36. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +2 -1
  37. package/node_modules/@agents-can-communicate/cli/src/main.mjs +19 -2
  38. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +25 -8
  39. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +13 -3
  40. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +19 -1
  41. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +53 -3
  42. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +5 -1
  43. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +7 -1
  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/native-reason.mjs +22 -0
  47. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -3
  48. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  49. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -1
  50. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -3
  51. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +123 -10
  52. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +11 -1
  54. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  55. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +3 -0
  56. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  58. package/package.json +1 -1
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // Private detached updater. It never opens workspace state or runs a model.
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { runWorker, recordWorkerFailure } from "@agents-can-communicate/cli/managed-worker";
5
6
  const [root, ...extra] = process.argv.slice(2);
7
+ // The generation this worker belongs to, so it reclaims only as the active one.
8
+ const generationRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
9
  if (typeof root === "string" && path.isAbsolute(root) && extra.length === 0) {
7
- await runWorker(root, { wait: true }).catch(error => recordWorkerFailure(root, error));
10
+ await runWorker(root, { wait: true, generationRoot }).catch(error => recordWorkerFailure(root, error));
8
11
  }
@@ -5,6 +5,7 @@ import { EXIT, createId } from "@agents-can-communicate/protocol";
5
5
  import { createCoordinationService } from "@agents-can-communicate/core";
6
6
  import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
7
7
  import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
8
+ import { registerDiscoveredSessions } from "@agents-can-communicate/hook-runner";
8
9
  import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
9
10
  import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
10
11
  from "@agents-can-communicate/cli";
@@ -60,6 +61,8 @@ await serve({
60
61
  participantId,
61
62
  descriptor,
62
63
  runtimeDir: paths.root,
64
+ registerNativeSessions: () => registerDiscoveredSessions({ adapters,
65
+ workspaceId: descriptor.id, dataHome, env: process.env }),
63
66
  },
64
67
  });
65
68
 
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
5
5
  import { createId } from "@agents-can-communicate/protocol";
6
6
  import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
7
7
  import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
8
+ import { registerDiscoveredSessions } from "@agents-can-communicate/hook-runner";
8
9
  import { ALL_ADAPTERS, askConfirmation, main as runCli } from "@agents-can-communicate/cli";
9
10
 
10
11
  export async function main({ managerRoot = null, packageRoot, managementOnly = false } = {}) {
@@ -27,6 +28,10 @@ const runtime = {
27
28
  createDeliveryRouter({ service, adapters, clock,
28
29
  readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
29
30
  adapterId: adapter.id }) }),
31
+ // Chats a client holds before their first turn, registered before status
32
+ // is read so a peer can address them (#167).
33
+ registerNativeSessions: ({ workspaceId, dataHome }) =>
34
+ registerDiscoveredSessions({ adapters, workspaceId, dataHome, env: process.env }),
30
35
  // Asked only by `acc config init`, and only when stdout is a terminal. There
31
36
  // was no port here at all, so the question went to the fallback that always
32
37
  // answers no: in a real terminal the command printed "not written" and never
@@ -192,6 +192,9 @@ transport that is technically different on some platform is the probe's fact to
192
192
  `docs/design/2026-09-26-native-delivery-across-platforms.md` in the repository.
193
193
  - Native methods return closed facts (`validateNativeActivationPlan()` closes the
194
194
  activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
195
+ A refused handshake may add `launchOption`: the bare command-line option, such as
196
+ `--search`, that made the client refuse. Doctor and the sender name it; a value, a vendor
197
+ string or anything but an option token is rejected.
195
198
 
196
199
  `nativeDelivery.policySource` accepts only `"installation-record"`, which is also the
197
200
  default. Hooks and senders read the current recorded recipient consent, independently of
@@ -231,8 +231,13 @@ store contract this one does, and run the active generation otherwise. Apart fro
231
231
  `SessionEnd`, only the client's confirmed death releases a pin, so a session whose client
232
232
  process cannot be found is not pinned, and a pin that names no client is reaped. A
233
233
  generation directory is reclaimed once no control pointer, live lease, live pin or staging
234
- hold names it. Parsed help, version, and update recovery remain available when workspace
235
- admission is unavailable.
234
+ hold names it. An update is run by the installed code, so right after an activation the
235
+ activated generation's own reclaim runs from its files, and its worker reclaims on every later
236
+ pass; each pass is recorded in `reclaim.json`. The updating process still runs from the
237
+ generation it replaced, and its lease keeps that one through the pass. Such a pass, like one an
238
+ unknown holder postponed, is recorded as unfinished, and the next ACC command an hour or more
239
+ later starts the worker that finishes it. Parsed help, version, and update recovery remain
240
+ available when workspace admission is unavailable.
236
241
 
237
242
  An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
238
243
  stable package identity and the discovered integrity, and health-checks the staged runtime.
package/docs/CLI.md CHANGED
@@ -122,7 +122,13 @@ participant for it again. A waiting participant's latest session stays listed as
122
122
  with that number as `waiting`, and with `endReason` when the client said why the session
123
123
  ended, such as `"clear"` for Claude Code's `/clear`. The text line names each one:
124
124
  `waiting for a closed session: claude_code-b (cleared, 2)`. `acc status --all` lists every
125
- session. Default `sync` reads
125
+ session.
126
+
127
+ Before it reads, `status` registers chats a client already holds but ACC has not seen, so a
128
+ peer can address a new Codex chat before anyone types in it. Codex's shared app-server daemon
129
+ lists them; a chat is registered when it is idle, is not a subagent, and ACC's hooks are enabled
130
+ and trusted in its directory. Its first turn, whether typed or started by a peer's message,
131
+ continues that session. `acc doctor` and the MCP `acc_status` tool do the same. Default `sync` reads
126
132
  events after a 16-digit event cursor (100 by
127
133
  default, up to 500). `--scope history` discovers historical message headers and reads
128
134
  selected records; `--scope full` adds an unbounded snapshot for explicit workspace forensics.
package/docs/CONCEPTS.md CHANGED
@@ -134,8 +134,8 @@ The durable inbox is universal. An adapter whose captures reach the installed cl
134
134
  also offer messages at the next normal turn. Codex LocalDaemon and the Claude Code inbox
135
135
  wake add experimental live delivery, off by default and subject to recipient policy and
136
136
  current reachability. A Claude Code wake makes the session run a turn, and that turn's
137
- next-turn hook shows the message. Codex can retain an eligible daemon thread after its
138
- terminal exits. [Capabilities](CAPABILITIES.md) gives the current limits.
137
+ next-turn hook shows the message. Codex can retain an eligible daemon thread for about a minute
138
+ after its terminal exits. [Capabilities](CAPABILITIES.md) gives the current limits.
139
139
 
140
140
  ## Handoffs preserve explicit context
141
141
 
@@ -149,8 +149,9 @@ never overrides them. See
149
149
 
150
150
  Codex LocalDaemon delivery separately requires Codex 0.152.1 or newer, a
151
151
  current feature probe, and exact thread, canonical cwd, process, version and protocol
152
- checks. On Codex 0.154.0 or newer, explicit complete setup can download a missing matching
153
- standalone package and prepare a definitely absent service. The setup choice names the
152
+ checks. On Codex 0.154.0 through 0.157.0, explicit complete setup can download a missing
153
+ matching standalone package and prepare a definitely absent service. Codex 0.157.1 and newer
154
+ install the service's own package when it first starts, so setup only starts it. The setup choice names the
154
155
  download. Existing service-start consent alone does not permit it. Codex files remain
155
156
  under the selected `CODEX_HOME`; the existing CLI command and shell profiles are preserved.
156
157
  Message delivery does not install Codex, start it, or stop it.
@@ -58,8 +58,9 @@ hooks wherever the client is at or after a captured version.
58
58
  An interactive install asks one default-No question for all selected clients that need a
59
59
  choice. Use `--delivery actionable|all` for explicit noninteractive consent. Use
60
60
  `--delivery off` to disable incoming automatic requests. A dry run does not ask, write
61
- configuration, download Codex, or start a service. On macOS or Linux with Codex 0.154.0 or newer,
62
- the choice includes downloading a missing standalone package from OpenAI. ACC selects the
61
+ configuration, download Codex, or start a service. On macOS or Linux with Codex 0.154.0 through
62
+ 0.157.0, the choice includes downloading a missing standalone package from OpenAI; Codex
63
+ 0.157.1 and newer install the service's own package when it first starts. ACC selects the
63
64
  matching version and keeps your existing npm or Homebrew command and shell profiles.
64
65
  When supported Codex service preparation succeeds, it
65
66
  means the infrastructure is ready. Start a Codex session and complete the client's hook and
@@ -146,13 +147,15 @@ Apple Silicon macOS. They are experimental and can spend tokens. Codex queues a
146
147
  until a running turn finishes. A busy Claude Code session takes the wake between two tool
147
148
  calls. Claude Code requires 2.1.282 or newer and recorded opt-in, and it wakes through the
148
149
  inbox that each session opens itself. Codex requires 0.152.1 or newer, LocalDaemon
149
- infrastructure, recorded opt-in, and a verified session. Supported explicit setup can
150
- prepare a missing service. Start the client with your normal command. A loaded daemon thread can receive
151
- messages after its terminal exits. Antigravity CLI 1.2.7 and later has one too: with delivery enabled,
152
- ACC's context asks the agent to start a relay from its own shell, up to three times while
153
- none is running, and you approve that command at the client's prompt. Antigravity also asks
154
- before each shell command, so install adds a rule that lets ACC's own commands run without
155
- that prompt, and a woken session can answer while you are away; see
150
+ infrastructure, recorded opt-in, and a verified session. A new Codex chat becomes
151
+ addressable before its first message: `acc status` registers it. Supported explicit setup can
152
+ prepare a missing service. Start the client with your normal command. A loaded daemon
153
+ thread can receive messages for about a minute after its terminal exits, until the daemon
154
+ unloads it and ACC closes its session. Antigravity CLI 1.2.7 and later has one too: with
155
+ delivery enabled, ACC's context asks the agent to start a relay from its own shell, up to
156
+ three times while none is running, and you approve that command at the client's prompt.
157
+ Antigravity also asks before each shell command, so install adds a rule that lets ACC's own
158
+ commands run without that prompt, and a woken session can answer while you are away; see
156
159
  [Antigravity CLI command approval](CONFIGURATION.md#antigravity-cli-command-approval).
157
160
  [Capabilities](CAPABILITIES.md) explains policy, versions and fallback.
158
161
 
@@ -115,7 +115,8 @@ All recipients have the same durable record, but adapters expose different accel
115
115
  minimum of 0.152.1 plus a current probe and exact session checks allow delivery
116
116
  to an independently opened thread. It is experimental, off by default, and can
117
117
  spend tokens. Messages wait for a running turn to finish; a daemon-retained
118
- thread can receive them after its terminal exits. ACC adds no launch arguments
118
+ thread can receive them for about a minute after its terminal exits, until the
119
+ daemon unloads it and ACC closes its session. ACC adds no launch arguments
119
120
  and does not manage the vendor daemon during message delivery. Separately,
120
121
  [confirmed update maintenance](UPGRADING.md#confirmed-client-service-maintenance) can restart it.
121
122
 
@@ -125,8 +125,9 @@ ambiguous recipients or failed identity checks retain durable inbox access.
125
125
  `native_endpoint_unavailable` means the local service endpoint is missing or is not a safe socket;
126
126
  `native_session_unavailable` means the service answered but has no loaded thread to probe.
127
127
  An interactive install can save consent in either case. ACC preserves it through a temporary
128
- outage. On Codex 0.154.0 or newer, an explicit install can download the missing official
129
- standalone package and prepare a missing service. The single setup choice covers the
128
+ outage. On Codex 0.154.0 through 0.157.0, an explicit install can download the missing official
129
+ standalone package and prepare a missing service. Codex 0.157.1 and newer install the
130
+ service's own package at its first start, and an explicit install only starts it. The single setup choice covers the
130
131
  download. Older consent for service start receives one expanded choice. To change a saved
131
132
  download refusal, run `acc install --adapter codex --delivery actionable` (or `all` to retain
132
133
  that policy). ACC preserves an existing npm or Homebrew command and shell profiles.
@@ -135,6 +136,21 @@ doctor. Download failure keeps the installed integration and its inbox fallback.
135
136
  service is infrastructure only. Open a new Codex session afterward and complete the
136
137
  client-owned hook and trust review.
137
138
 
139
+ After a reboot no Codex service runs. Codex 0.157.1 and newer start it when the next Codex
140
+ session opens (`daemon_auto_start`), and doctor then says so instead of advising `acc install`.
141
+ A Codex chat that started without the service runs on its own embedded service until it closes,
142
+ and no peer can reach it live. ACC reports it as `client_session_embedded`: the chat shows the
143
+ user a notice once, a sender sees the cause beside `no live transport`, and doctor names it on
144
+ the session line. Open a new Codex chat while the service runs; start it with
145
+ `codex app-server daemon start` if Codex does not.
146
+
147
+ Some launch options make Codex run a chat embedded even while the service runs: `-c`/`--config`,
148
+ `--enable`, `--disable`, `--search`, `-p`/`--profile`, `--oss`, `--strict-config`,
149
+ `--dangerously-bypass-hook-trust` and `--no-daemon`, and also the `CODEX_EXEC_SERVER_URL`
150
+ environment variable. ACC names the option in the notice, the sender's line and doctor. For live
151
+ delivery, start Codex without it; put a setting you need in `config.toml` instead of `-c`,
152
+ `--enable`, `--disable`, `--search` or `--profile`.
153
+
138
154
  If a reply stays queued with `transport_permission_denied`, the sender's permissions
139
155
  blocked local transport. This is distinct from an unavailable recipient. The message was
140
156
  recorded successfully; the error does not mean it was read or acknowledged.
package/docs/UPGRADING.md CHANGED
@@ -39,6 +39,41 @@ npm_config_prefer_online=true acc update
39
39
 
40
40
  From 0.8.3, the download does this itself.
41
41
 
42
+ 0.8.4 changes what you see around Codex and after an update:
43
+
44
+ - **A new Codex chat is addressable before anyone types in it.** `acc status`, `acc doctor` and
45
+ the MCP `acc_status` tool register chats that Codex's daemon holds, when ACC's hooks are
46
+ enabled and trusted in their directory. The chat's first turn continues that session.
47
+ - **A Codex chat that runs without the daemon says so.** It is told once that live delivery is
48
+ off; a sender sees the cause beside `no live transport`. After a reboot, doctor advises
49
+ opening Codex rather than `acc install` when Codex starts its own service.
50
+ - **A new `CODEX_HOME` on Codex 0.157.1 or newer needs no standalone download.** `acc install`
51
+ only starts the service; Codex installs its own package.
52
+ - **One background pass after the update from 0.8.3.** The first client hook or agent command
53
+ after the update starts the new version's update worker once, even with automatic updates
54
+ off, to reclaim old runtime generations with the new rule, and records `reclaim.json` in the
55
+ runtime directory. These run through ACC's own launchers; an `acc` that npm installed
56
+ globally keeps the entry of the version npm installed, so it does not start this pass.
57
+
58
+ This release adds no store event type or field.
59
+
60
+ 0.8.5 changes what a Codex chat that runs without the daemon is told:
61
+
62
+ - **The launch option that caused it is named.** Codex also runs a chat embedded, even while
63
+ its daemon runs, when it starts with `-c`/`--config`, `--enable`, `--disable`, `--search`,
64
+ `-p`/`--profile`, `--oss`, `--strict-config`, `--dangerously-bypass-hook-trust` or
65
+ `--no-daemon`. The notice, a sender's line and doctor name the option and advise starting
66
+ Codex without it; put a setting you need in `config.toml` instead.
67
+ - **You see the notice yourself.** Codex shows it once in the chat as a hook message
68
+ (`↳ Hook · ACC: …`); the model receives its own line, as before.
69
+ - **The previous runtime generation goes an hour after the update.** `acc update` runs from
70
+ the generation it replaces, so it cannot remove that one itself. It records the reclaim as
71
+ unfinished in `reclaim.json`, and the first ACC command an hour or more later removes the old
72
+ generation, with automatic updates on or off.
73
+
74
+ This release adds no store event type or field. A sender's delivery JSON may carry
75
+ `nativeLaunchOption` beside `nativeReasonCode`.
76
+
42
77
  ## From 0.7.x
43
78
 
44
79
  Claude Code live delivery wakes each session through the inbox socket that Claude Code opens
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-antigravity",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "acc",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Coordinate this Antigravity CLI session with other AI agent sessions working in the same workspace."
5
5
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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.8.3",
3
+ "version": "0.8.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,9 +1,11 @@
1
1
  import { defineAdapter, projectContext, projectContextResult }
2
2
  from "@agents-can-communicate/adapter-sdk";
3
- import { probeNativeDelivery, planNativeActivation, bindNativeSession, refreshNativeSession, retireNativeSession, offerMessage } from "./native-delivery.mjs";
3
+ import { probeNativeDelivery, planNativeActivation, bindNativeSession, refreshNativeSession, retireNativeSession, offerMessage,
4
+ nativeActivationHint } from "./native-delivery.mjs";
4
5
  import certification from "../certification.json" with { type: "json" };
5
6
  import { createCodexServiceSetup } from "./service-setup.mjs";
6
7
  import { createCodexMaintenance } from "./maintenance.mjs";
8
+ import { createCodexDiscovery } from "./native-discovery.mjs";
7
9
  export { sameMaintenanceIdentity } from "./maintenance.mjs";
8
10
 
9
11
  import { CODEX_QUEUE_MINIMUM, PROTOCOL_CONTRACT } from "./app-server-client.mjs";
@@ -68,10 +70,14 @@ export function createCodexAdapter() {
68
70
  },
69
71
  probeNativeDelivery, planNativeActivation, bindNativeSession,
70
72
  refreshNativeSession, retireNativeSession, offerMessage,
73
+ // A chat that runs embedded is told once why nothing reaches it live.
74
+ nativeActivationHint,
71
75
  // A Codex sender reaches another Codex session through its control socket.
72
76
  inboundSockets: context => [controlSocket(context)],
73
77
  ...createCodexMaintenance(),
74
78
  ...createCodexServiceSetup(),
79
+ // Chats the daemon holds before their first turn, for registration (#167).
80
+ ...createCodexDiscovery(),
75
81
  startSession: async () => ({ ok: true, changes: [], diagnostics: [] }),
76
82
  endSession: async () => ({ ok: true, changes: [], diagnostics: [] }),
77
83
  guardWrite: async () => ({ ok: true, changes: [], diagnostics: [] }),
@@ -9,7 +9,7 @@ import { openWebSocketPeer } from "./ws-json-rpc.mjs";
9
9
  // The Codex App Server queue protocol, captured on codex-cli 0.152.1. Every
10
10
  // method here is official and present in the generated schema: initialize,
11
11
  // thread/loaded/list, thread/list, metadata-only thread/read, thread/queue/list,
12
- // thread/queue/add. Thread history is never requested; metadata reads explicitly
12
+ // thread/queue/add, hooks/list. Thread history is never requested; metadata reads explicitly
13
13
  // exclude turns. Closed safe results only; no vendor string escapes to core.
14
14
 
15
15
  export const PROTOCOL_CONTRACT = "codex-app-server-thread-queue-v1";
@@ -109,6 +109,29 @@ function threadEntries(items) {
109
109
  return items;
110
110
  }
111
111
 
112
+ // What discovery may know about a loaded thread, and nothing more: never the
113
+ // preview, never a turn. A thread the daemon cannot describe is left out.
114
+ const THREAD_METADATA = ["id", "cwd", "status", "parentThreadId", "ephemeral"];
115
+ export async function loadedThreadMetadata(peer) {
116
+ const threads = [];
117
+ for (const threadId of await pageAll(peer, "thread/loaded/list", {})) {
118
+ if (typeof threadId !== "string" || threadId === "") throw protocolError();
119
+ const thread = (await peer.request("thread/read", { threadId, includeTurns: false }))?.thread;
120
+ if (thread?.id !== threadId) continue;
121
+ threads.push(Object.fromEntries(THREAD_METADATA.map(key => [key, thread[key] ?? null])));
122
+ }
123
+ return threads;
124
+ }
125
+
126
+ // Each directory's hooks as this daemon would run them, with their enablement
127
+ // and trust (`hooks/list`, present in the 0.158.0 generated schema).
128
+ export async function hooksForDirectories(peer, cwds) {
129
+ const response = await peer.request("hooks/list", { cwds });
130
+ if (!Array.isArray(response?.data)) throw protocolError();
131
+ return response.data.map(entry => ({ cwd: entry?.cwd,
132
+ hooks: Array.isArray(entry?.hooks) ? entry.hooks : [] }));
133
+ }
134
+
112
135
  export async function canonicalCwd(cwd) {
113
136
  if (typeof cwd !== "string" || !path.isAbsolute(cwd) || cwd.includes("\0")) return null;
114
137
  try {
@@ -0,0 +1,108 @@
1
+ import path from "node:path";
2
+
3
+ import { runMaintenanceCommand } from "./maintenance-host.mjs";
4
+
5
+ // Which process runs a hook tells a daemon-hosted thread from an embedded chat.
6
+ // Measured on 0.158.0: a daemon thread's hook runs under
7
+ // `codex app-server --listen unix:// --managed-daemon`, while a TUI that found
8
+ // no daemon (and did not start one) hosts its own app server and runs the hook
9
+ // itself - for the rest of its life, since a later daemon never adopts it.
10
+
11
+ // Every subcommand `codex --help` lists on 0.158.0 that runs no interactive
12
+ // chat. `resume` and `fork` open one, so they are absent.
13
+ const NOT_INTERACTIVE = new Set(["exec", "e", "review", "login", "logout", "mcp", "mcp-server",
14
+ "plugin", "app-server", "remote-control", "app", "completion", "update", "doctor", "sandbox",
15
+ "debug", "apply", "a", "queue", "archive", "delete", "migrate-rollouts", "unarchive", "cloud",
16
+ "exec-server", "features", "agents"]);
17
+ // Options whose value is the next word, so that word is not a subcommand.
18
+ const VALUE_OPTIONS = new Set(["-c", "--config", "-m", "--model", "-p", "--profile", "-s",
19
+ "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "-i", "--image", "--add-dir", "--enable",
20
+ "--disable", "--local-provider"]);
21
+
22
+ export async function hostArgv(pid) {
23
+ const result = await runMaintenanceCommand("/bin/ps", ["-o", "args=", "-p", String(pid)],
24
+ { timeout: 500 });
25
+ if (result.status !== 0) throw new Error("host process unreadable");
26
+ return result.stdout.trim().split(/\s+/).filter(Boolean);
27
+ }
28
+
29
+ // The arguments after Codex itself, or null for any process that is not Codex:
30
+ // `codex ...`, or a node script named codex, as `node [options] codex.js ...`.
31
+ function codexArguments(argv) {
32
+ const [program, ...rest] = argv;
33
+ if (path.basename(program ?? "") === "codex") return rest;
34
+ if (path.basename(program ?? "") !== "node") return null;
35
+ const script = rest.findIndex(word => !word.startsWith("-"));
36
+ return script >= 0 && path.basename(rest[script]).replace(/\.[mc]?js$/, "") === "codex"
37
+ ? rest.slice(script + 1) : null;
38
+ }
39
+
40
+ /**
41
+ * Whether this command line is an interactive Codex TUI: no subcommand,
42
+ * `resume`, `fork`, or a prompt. `ps` drops quoting, so a prompt whose first
43
+ * word names a subcommand reads as not interactive - the safe direction, which
44
+ * only withholds a notice.
45
+ */
46
+ export function interactiveHost(argv) {
47
+ const words = codexArguments(argv);
48
+ if (words === null) return false;
49
+ let value = false;
50
+ for (const word of words) {
51
+ if (value) { value = false; continue; }
52
+ if (word.startsWith("-")) { value = VALUE_OPTIONS.has(word); continue; }
53
+ return !NOT_INTERACTIVE.has(word);
54
+ }
55
+ return true;
56
+ }
57
+
58
+ // Options that make Codex run a chat on its own embedded app server even while
59
+ // the daemon runs. Measured on 0.159.1, which says so only behind its status
60
+ // line: "Running without the shared background server: command-line
61
+ // configuration overrides (-c, --enable, --disable, or --search) requires
62
+ // embedded mode." The same sentence names --profile, --oss, --strict-config,
63
+ // --dangerously-bypass-hook-trust and --no-daemon.
64
+ const EMBEDDING_OPTIONS = new Set(["-c", "--config", "--enable", "--disable", "--search", "-p",
65
+ "--profile", "--oss", "--strict-config", "--dangerously-bypass-hook-trust", "--no-daemon"]);
66
+ const OPENS_CHAT = new Set(["resume", "fork"]);
67
+
68
+ // `--config=x` and `-cx` carry their value; both name the option they spell.
69
+ const optionName = word => word.startsWith("--") ? word.split("=", 1)[0] : word.slice(0, 2);
70
+
71
+ /**
72
+ * The first option on this interactive command line that keeps the chat
73
+ * embedded, as typed, or null. Scanning stops at the prompt: `ps` drops quotes,
74
+ * so a later word may be the prompt's own text.
75
+ */
76
+ export function embeddingOption(argv) {
77
+ const words = codexArguments(argv);
78
+ if (words === null) return null;
79
+ let value = false;
80
+ let opened = false;
81
+ for (const word of words) {
82
+ if (value) { value = false; continue; }
83
+ // `--` ends the options: what follows is the prompt, however it reads.
84
+ if (word === "--") return null;
85
+ if (word.startsWith("-")) {
86
+ if (EMBEDDING_OPTIONS.has(optionName(word))) return optionName(word);
87
+ value = VALUE_OPTIONS.has(word);
88
+ continue;
89
+ }
90
+ if (opened || !OPENS_CHAT.has(word)) return null;
91
+ opened = true;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * For a host that was read and is an interactive TUI, `{ launchOption }` - the
98
+ * option that keeps it embedded, or null when its command line shows none.
99
+ * Null for any other host or an unreadable one; never throws.
100
+ */
101
+ export async function embeddedHost(pid, argvOf = hostArgv) {
102
+ try {
103
+ const argv = await argvOf(pid);
104
+ return interactiveHost(argv) ? { launchOption: embeddingOption(argv) } : null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
@@ -138,15 +138,25 @@ export function allowOutcome() {
138
138
  /**
139
139
  * Context for the next turn.
140
140
  *
141
- * This client wraps nothing: a UserPromptSubmit hook's stdout arrives at the
142
- * model as a `developer` role message, verbatim. So the injection is plain
143
- * text - emitting Claude Code's JSON envelope would put the envelope itself
144
- * into the conversation, the same mistake it would be on Kimi Code.
141
+ * This client wraps nothing: a UserPromptSubmit hook's plain stdout arrives at
142
+ * the model as a `developer` role message, verbatim, so the ordinary injection
143
+ * is plain text. Codex also parses a JSON answer - 0.147.0, 0.155.1 and 0.159.1
144
+ * took `hookSpecificOutput.additionalContext` into the same developer message
145
+ * and kept the envelope out of the conversation, measured 2026-09-29 - which
146
+ * is how a notice for the user rides beside the context.
145
147
  *
146
148
  * The developer role is the most direct channel of the four, which is a reason
147
149
  * to be careful with what goes into it: anything a peer wrote must stay framed
148
150
  * as data, because at this role the model reads text as instruction.
149
151
  */
150
- export function injectOutcome(context) {
151
- return { stdout: context, stderr: "", exitCode: 0 };
152
+ export function injectOutcome(context, notice) {
153
+ if (typeof notice?.userMessage !== "string") return { stdout: context, stderr: "", exitCode: 0 };
154
+ // A line the user must see rides as the hook's systemMessage, which Codex
155
+ // shows in the chat and never sends to the model; the context goes to the
156
+ // model as additionalContext, the same developer message plain text becomes.
157
+ // Measured with a probe hook on 0.147.0, 0.155.1 and 0.159.1. Only the ask's
158
+ // one turn prints JSON; every other turn keeps the plain text above.
159
+ return { stdout: JSON.stringify({ systemMessage: notice.userMessage,
160
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }),
161
+ stderr: "", exitCode: 0 };
152
162
  }
@@ -32,6 +32,8 @@ const PLUGIN_NAME = "agents-can-communicate";
32
32
  // again to confirm a root of ACC's own is accepted and reported enabled.
33
33
  const MARKETPLACE = "acc-local";
34
34
  const QUALIFIED = `${PLUGIN_NAME}@${MARKETPLACE}`;
35
+ // The id Codex reports for ACC's plugin, in `hooks/list` among other places.
36
+ export const CODEX_PLUGIN_ID = QUALIFIED;
35
37
  const HOOK_REVIEW = "hook readiness unverified: open codex; check ACC is enabled in /plugins; in /hooks, review "
36
38
  + "each ACC hook and enable/trust its current definition if needed, then restart the session";
37
39
 
@@ -12,8 +12,19 @@ export const failMaintenance = reasonCode => { throw Object.assign(new Error(rea
12
12
  const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
13
13
  export const startTimeValid = value => typeof value === "string"
14
14
  && /^[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d\d:\d\d:\d\d \d{4}$/.test(value);
15
- export const managedExecutablePaths = codexHome => ["bin/codex", "codex"].map(name =>
16
- path.join(codexHome, "packages/standalone/current", name));
15
+ // The two packages a Codex daemon runs from, measured. The standalone install
16
+ // keeps its PID in app-server.pid (0.154.0-0.159.0); a home with no standalone
17
+ // package gets the daemon's own package from `app-server daemon start` on
18
+ // 0.157.1 and newer, with its PID in daemon.pid (0.159.0, 2026-09-29). A home
19
+ // that has the standalone package keeps using it, so it is looked for first.
20
+ const DAEMON_LAYOUTS = Object.freeze([
21
+ { name: "standalone", packageDir: "packages/standalone", executables: ["current/bin/codex", "current/codex"],
22
+ pidFile: "app-server-daemon/app-server.pid" },
23
+ { name: "self-installed", packageDir: "packages/app-server-daemon", executables: ["current/bin/codex"],
24
+ pidFile: "app-server-daemon/daemon.pid" },
25
+ ]);
26
+ export const managedExecutablePaths = codexHome => DAEMON_LAYOUTS.flatMap(layout =>
27
+ layout.executables.map(name => path.join(codexHome, layout.packageDir, name)));
17
28
 
18
29
  // Where lsof lives: /usr/sbin on macOS, /usr/bin on Linux. A fixed list of
19
30
  // absolute paths, never PATH, so a directory an operator can write to cannot
@@ -57,15 +68,28 @@ export async function maintenanceContext(context = {}) {
57
68
  let codexHome = env.CODEX_HOME ?? path.join(home, ".codex");
58
69
  if (!absolute(home) || !absolute(codexHome)) failMaintenance("invalid_service_home");
59
70
  codexHome = await canonicalFuturePath(codexHome);
60
- const [binPath, legacyPath] = managedExecutablePaths(codexHome);
61
- const managedPath = await metadataExists(binPath) ? binPath : legacyPath;
71
+ const { layout, managedPath } = await installedLayout(codexHome);
62
72
  return { codexHome, socketPath: path.join(codexHome, "app-server-control/app-server-control.sock"),
63
- pidPath: path.join(codexHome, "app-server-daemon/app-server.pid"),
73
+ pidPath: path.join(codexHome, layout.pidFile), layout: layout.name,
74
+ packageDir: path.join(codexHome, layout.packageDir),
64
75
  managedPath,
65
76
  platform: context.platform ?? `${process.platform}-${process.arch}`,
66
77
  options: { cwd: codexHome, env: { ...env, HOME: home, CODEX_HOME: codexHome } } };
67
78
  }
68
79
 
80
+ // The first layout whose executable exists. With none installed, the
81
+ // standalone layout's legacy path, which service setup reports as missing.
82
+ async function installedLayout(codexHome) {
83
+ for (const layout of DAEMON_LAYOUTS) {
84
+ for (const name of layout.executables) {
85
+ const managedPath = path.join(codexHome, layout.packageDir, name);
86
+ if (await metadataExists(managedPath)) return { layout, managedPath };
87
+ }
88
+ }
89
+ const [standalone] = DAEMON_LAYOUTS;
90
+ return { layout: standalone, managedPath: path.join(codexHome, standalone.packageDir, "current/codex") };
91
+ }
92
+
69
93
  export async function metadataExists(file) {
70
94
  try { await lstat(file); return true; }
71
95
  catch (error) { if (error.code === "ENOENT") return false; throw error; }
@@ -143,16 +167,16 @@ export function socketListedIn(stdout, socketPaths) {
143
167
 
144
168
  // The daemon's command line as measured: 0.154.0 ran `<current path> app-server
145
169
  // --listen unix://`; 0.157.1 runs the resolved releases path with
146
- // `--managed-daemon` appended. The executable must live in the standalone tree
147
- // and be the managed binary itself, however the tree names it.
170
+ // `--managed-daemon` appended. The executable must live in the package tree of
171
+ // the installed layout and be the managed binary itself, however it is named.
148
172
  const DAEMON_COMMAND = /^(\/.+?) app-server --listen unix:\/\/(?: --managed-daemon)?$/;
149
173
 
150
174
  export async function verifyMaintenanceProcess(snapshot, paths, run, { lsof = resolveLsof } = {}) {
151
175
  const process = await observeMaintenanceProcess(snapshot.pid, paths, run);
152
176
  const executable = DAEMON_COMMAND.exec(process.command ?? "")?.[1] ?? null;
153
- const standalone = path.join(paths.codexHome, "packages", "standalone") + path.sep;
177
+ const packages = (paths.packageDir ?? path.join(paths.codexHome, "packages", "standalone")) + path.sep;
154
178
  if (process.state !== "alive" || process.processStartTime !== snapshot.processStartTime
155
- || executable === null || !executable.startsWith(standalone)
179
+ || executable === null || !executable.startsWith(packages)
156
180
  || await realpath(executable).catch(() => null) !== await realpath(paths.managedPath)) {
157
181
  failMaintenance("daemon_identity_unavailable");
158
182
  }