agents-can-communicate 0.7.1 → 0.8.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 (105) hide show
  1. package/README.md +3 -1
  2. package/bin/entrypoints/acc-bootstrap.mjs +6 -48
  3. package/bin/entrypoints/acc-claude-channel.mjs +47 -157
  4. package/bin/entrypoints/acc-mcp.mjs +0 -1
  5. package/bin/entrypoints/acc.mjs +0 -1
  6. package/docs/ADAPTER_AUTHORING.md +74 -24
  7. package/docs/ARCHITECTURE.md +31 -18
  8. package/docs/CAPABILITIES.md +38 -25
  9. package/docs/CLI.md +51 -13
  10. package/docs/CONCEPTS.md +5 -4
  11. package/docs/CONFIGURATION.md +18 -11
  12. package/docs/GETTING_STARTED.md +15 -12
  13. package/docs/GLOSSARY.md +2 -1
  14. package/docs/HOW_IT_WORKS.md +8 -4
  15. package/docs/PROTOCOL.md +25 -6
  16. package/docs/RELEASING.md +8 -5
  17. package/docs/SECURITY_MODEL.md +14 -1
  18. package/docs/TROUBLESHOOTING.md +66 -29
  19. package/docs/UPGRADING.md +49 -8
  20. package/docs/WHY_ACC.md +5 -3
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +2 -3
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +26 -30
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +249 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
  38. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -4
  40. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
  41. package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
  42. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  50. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  51. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
  56. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
  57. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
  58. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +104 -84
  59. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
  60. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  61. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
  62. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
  63. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
  64. package/node_modules/@agents-can-communicate/cli/src/main.mjs +24 -3
  65. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
  66. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  67. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
  68. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
  69. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
  70. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
  71. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  72. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
  73. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
  74. package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
  75. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  76. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
  77. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +33 -48
  78. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  79. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
  80. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
  81. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +27 -15
  82. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
  83. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +4 -2
  84. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  85. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
  86. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
  87. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
  88. package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
  89. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
  90. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
  91. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
  92. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
  93. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  95. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  96. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
  97. package/package.json +1 -1
  98. package/bin/acc-bootstrap.mjs +0 -7
  99. package/bin/acc-claude-channel.mjs +0 -10
  100. package/bin/entrypoints/claude-channel-binding.mjs +0 -46
  101. package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
  102. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
  103. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
  104. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
  105. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
package/README.md CHANGED
@@ -98,7 +98,9 @@ provide messages at the next normal turn; other sessions read their ACC inbox ex
98
98
 
99
99
  Experimental live delivery can wake eligible Claude Code and Codex sessions on Apple Silicon
100
100
  macOS. It requires opt-in and an active verified connection, is off by default, and can spend
101
- model tokens. Messages for busy sessions queue until the current turn ends.
101
+ model tokens. Claude Code sessions wake through the inbox that Claude Code itself opens, so
102
+ you start Claude Code with your ordinary command. A busy Claude Code session gets the message
103
+ between two tool calls. A busy Codex session gets it when the current turn ends.
102
104
 
103
105
  See [client capabilities](docs/CAPABILITIES.md) for exact support. Run `acc doctor` from your
104
106
  project if a peer is missing or delivery differs from what you expect; see
@@ -1,51 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Internal entry point behind an ACC-owned shell shim: is this exact vendor
3
- // executable eligible for native delivery right now? Exit 0 means yes and the
4
- // shim exports its owned policy before `exec`; any other exit - including a
5
- // crash, a missing adapter, or a damaged install - means the shim launches the
6
- // vendor command untouched. Nothing is ever written to stdout, and stderr stays
7
- // empty unless ACC_BOOTSTRAP_DEBUG=1 asks for one safe line.
8
- import { checkNativeBootstrap } from "@agents-can-communicate/installer";
9
-
10
- import { createAntigravityAdapter } from "@agents-can-communicate/adapter-antigravity";
11
- import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
12
- import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
13
- import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
14
- import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
15
- import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
16
-
17
- import { parseBootstrapOptions } from "@agents-can-communicate/cli/managed-entry";
18
-
19
- function debug(line) {
20
- if (process.env.ACC_BOOTSTRAP_DEBUG === "1") process.stderr.write(`acc-bootstrap: ${line}\n`);
21
- }
22
-
2
+ // Kept only for installs made by ACC 0.7.x or earlier. A 0.7.x updater refuses a
3
+ // downloaded package without this file, and a `claude` shell shim written by
4
+ // 0.7.x still runs it through its launcher until `acc install` or `acc update`
5
+ // retires the shim. A shim reads any exit other than 0 as "launch the vendor
6
+ // command untouched", so this answers 1 and writes nothing.
23
7
  export async function main() {
24
- try {
25
- const options = parseBootstrapOptions(process.argv.slice(2));
26
- if (options === null) {
27
- debug("usage: --adapter <id> --real-executable <path> --data-home <path>");
28
- process.exit(2);
29
- }
30
- const registry = {
31
- claude_code: createClaudeCodeAdapter, codex: createCodexAdapter,
32
- gemini_cli: createGeminiCliAdapter, grok: createGrokAdapter, kimi: createKimiAdapter,
33
- // No native delivery contract: this reports the client unsupported rather
34
- // than unknown, which is a different and more useful thing to read.
35
- antigravity: createAntigravityAdapter,
36
- };
37
- const adapter = registry[options.adapter]?.();
38
- if (adapter === undefined) {
39
- debug(`${options.adapter}: unknown adapter`);
40
- process.exit(1);
41
- }
42
- const result = await checkNativeBootstrap({ adapter, realExecutable: options.realExecutable,
43
- platform: `${process.platform}-${process.arch}`, dataHome: options.dataHome, timeoutMs: 750 });
44
- debug(`${options.adapter}: ${result.supported ? "supported" : result.reasonCode}`);
45
- process.exit(result.supported ? 0 : 1);
46
- } catch (error) {
47
- debug(`failed: ${String(error?.code ?? error?.name ?? "error")}`);
48
- process.exit(1);
49
- }
50
-
8
+ process.exitCode = 1;
51
9
  }
@@ -1,166 +1,56 @@
1
1
  #!/usr/bin/env node
2
- // The ACC Channel binary Claude Code spawns as a stdio MCP child. It composes
3
- // the same workspace discovery, session binding, core service, and local
4
- // data-home rules as acc-hook, opens a session-scoped endpoint for the delivery
5
- // router, and turns the model's explicit acc_reply / acc_ack into real ACC
6
- // records. It never collects a transcript or runs a model, and it fails open:
7
- // any composition error leaves Claude a plain MCP server that simply offers no
8
- // tools rather than crashing the session.
9
- import { randomBytes } from "node:crypto";
10
-
11
- import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
12
- import { listSessionBindings } from "@agents-can-communicate/adapter-sdk";
13
- import { createCoordinationService } from "@agents-can-communicate/core";
14
- import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
15
- import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
16
- import { recordAndOffer } from "@agents-can-communicate/mcp-server";
17
- import { resolveClientPid } from "@agents-can-communicate/hook-runner/client-pid";
18
- import { readProcessTable } from "@agents-can-communicate/hook-runner/process-table";
19
- import { createId } from "@agents-can-communicate/protocol";
20
- import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
21
- import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
22
- from "@agents-can-communicate/cli";
23
-
24
- import { createAccChannel, endpointDir, routeAck, routeReply }
25
- from "@agents-can-communicate/adapter-claude-code/channel";
26
-
27
- import { pump, startInertChannel } from "./claude-channel-stdio.mjs";
28
- import { activateClaudeChannel } from "./claude-channel-binding.mjs";
29
-
30
- const clock = { now: () => new Date().toISOString() };
31
- const ids = { next: kind => createId(kind, randomBytes) };
32
-
33
- /**
34
- * The ACC session this Claude process opened - decided by which client process
35
- * this Channel is running under, never by how many sessions happen to be
36
- * visible.
37
- *
38
- * There used to be a fallback here: if exactly one session was live, it was
39
- * taken to be this one. On a single-session machine that looked safe, and with
40
- * two sessions it silently handed one client the other's identity. The Channel
41
- * starts while its own SessionStart hook is still writing its binding, so the
42
- * only binding a second client can see at that moment is the *first* client's -
43
- * and the wait that was added to survive that race then locks the wrong answer
44
- * in on the first attempt, because it is a complete, live, plausible binding.
45
- *
46
- * Measured on two real 2.1.259 sessions: both Channels registered an endpoint
47
- * under the first client's pid. `bindNativeSession` refuses two registrations
48
- * for one pid, so the session that had been receiving live messages dropped
49
- * back to the durable inbox - and an `acc_reply` from the second window would
50
- * have been recorded as the first session's answer, which is a false record
51
- * rather than a lost message.
52
- *
53
- * This process was spawned by its client, so its own ancestry names that
54
- * client, and no race can change that. Ownership is decided by it.
55
- */
56
- export async function resolveSession({ runtimeDir, service, env, ownClientPid,
57
- listBindings = listSessionBindings }) {
58
- if (!Number.isInteger(ownClientPid)) return null;
59
- const bindings = await listBindings({ runtimeDir });
60
- if (bindings.length === 0) return null;
61
- const status = await service.collectStatus({});
62
- const live = new Set(status.participants
63
- .filter(participant => participant.presence !== "offline")
64
- .map(participant => participant.sessionId));
65
- const mine = bindings.filter(binding => live.has(binding.accSessionId)
66
- && binding.clientPid === ownClientPid);
67
- // One client process can outlive a session and open another, so the pid alone
68
- // can still name two. The harness session id the client exports separates
69
- // them; with nothing to separate them the answer is nobody, as before.
70
- const exported = new Set(Object.values(env ?? {}).filter(value => typeof value === "string"));
71
- const matched = mine.filter(binding => exported.has(binding.harnessSessionId));
72
- const chosen = mine.length === 1 ? mine[0] : matched.length === 1 ? matched[0] : null;
73
- return chosen === null ? null
74
- : { sessionId: chosen.accSessionId, generation: chosen.generation, clientPid: chosen.clientPid,
75
- harnessSessionId: chosen.harnessSessionId };
76
- }
77
-
78
- /** The client process this Channel was spawned by, or null when nobody knows. */
79
- export async function ownClient({ table = null, from = process.pid,
80
- command = createClaudeCodeAdapter().client.command } = {}) {
81
- return resolveClientPid({ table: table ?? await readProcessTable(), from, command });
82
- }
83
-
84
- // How long the Channel will wait for the hook to publish this session's
85
- // binding. Claude spawns this child while SessionStart is still running - both
86
- // landed in the same second on a real 2.1.259 launch - and a single lookup
87
- // loses that race often enough to make native delivery activate or not per
88
- // launch. Bounded, because a session with no ACC binding must still be answered.
89
- const BINDING_WAIT_MS = 5_000;
90
- const BINDING_POLL_MS = 100;
91
-
92
- /**
93
- * The session binding, once it exists - or null when the wait runs out.
94
- *
95
- * A binding with no `clientPid` names no process, and the Channel binds an
96
- * exact one, so it does not count as resolved: the hook writes the identity
97
- * first and the certified facts after, and reading in between must keep waiting
98
- * rather than settle for the half-written answer.
99
- */
100
- export async function resolveWithin({ resolve, deadline, intervalMs = BINDING_POLL_MS,
101
- now = () => Date.now(), sleep = ms => new Promise(resolve_ => setTimeout(resolve_, ms)) }) {
102
- for (;;) {
103
- const session = await resolve();
104
- if (session !== null && Number.isInteger(session?.clientPid)) return session;
105
- if (now() >= deadline) return null;
106
- await sleep(intervalMs);
2
+ // Kept only for installs made by ACC 0.7.x or earlier, which wired a Claude Channel MCP
3
+ // server. A 0.7.x updater refuses a downloaded package without this file, and a
4
+ // plugin copy written by 0.7.x can still ask Claude Code to spawn it until
5
+ // `acc install` or `acc update` lays the plugin out again.
6
+ //
7
+ // A child that answers nothing is reported to the user as a server that failed
8
+ // to connect, so this finishes the MCP handshake, declares no Channel and
9
+ // offers no tools, and exits when Claude closes its stdin or stops it with a signal.
10
+
11
+ function answer(message) {
12
+ if (message.method === "initialize") {
13
+ return { protocolVersion: message.params?.protocolVersion, capabilities: { tools: {} },
14
+ serverInfo: { name: "agents-can-communicate", version: "0.2.0" } };
107
15
  }
16
+ if (message.method === "tools/list") return { tools: [] };
17
+ if (message.method === "ping") return {};
18
+ return undefined;
108
19
  }
109
20
 
110
- async function compose() {
111
- const descriptor = await discoverWorkspace({ cwd: process.cwd(), env: process.env,
112
- gitProbe: createGitProbe() });
113
- const dataHome = platformDataHome({ platform: process.platform, env: process.env });
114
- const paths = runtimePaths({ dataHome, workspaceId: descriptor.id, workspaceRoots: descriptor.roots });
115
- const store = await openFilesystemStore({ root: paths.root, clock, ids,
116
- workspaceId: descriptor.id });
117
- const service = createCoordinationService({ store, clock, ids });
118
- const write = payload => process.stdout.write(`${JSON.stringify(payload)}\n`);
119
- // Read once: the ancestry that decides ownership cannot change while this
120
- // process runs, and re-reading it on every poll would spawn `ps` in a loop.
121
- const clientPid = await ownClient();
122
- const session = await resolveWithin({
123
- resolve: () => resolveSession({ runtimeDir: paths.root, service, env: process.env,
124
- ownClientPid: clientPid }),
125
- deadline: Date.now() + BINDING_WAIT_MS });
126
- // No binding to serve - but Claude is already speaking MCP to this child, and
127
- // returning here left the event loop empty: the process exited in 75ms
128
- // without answering `initialize`, and Claude reported the server as failed to
129
- // connect on every session that enables the plugin without ACC's shim.
130
- if (session === null || !Number.isInteger(session.clientPid)) {
131
- return startInertChannel({ write });
21
+ function handle(line) {
22
+ let message;
23
+ try { message = JSON.parse(line); } catch {
24
+ return { jsonrpc: "2.0", error: { code: -32700, message: "parse error" } };
132
25
  }
133
-
134
- const deliveryRouter = createDeliveryRouter({ service, adapters: ALL_ADAPTERS(), clock,
135
- readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome, adapterId: adapter.id }) });
136
- const channel = createAccChannel({
137
- endpointDir: endpointDir(paths.root),
138
- clientPid: session.clientPid,
139
- write,
140
- // The Channel tool records through its own bound session, then uses the
141
- // same durable-first offer as ordinary MCP. A successful reply record alone
142
- // does not deliver it to the peer, and a failed offer must not lose it.
143
- routeReply: ({ messageId, body }) => recordAndOffer({ router: deliveryRouter,
144
- selectMessage: value => value.reply,
145
- record: () => routeReply({ service, session, messageId, body }) }),
146
- routeAck: ({ messageId }) => routeAck({ service, session, messageId }),
147
- // This process is the only party that knows the endpoint is still being
148
- // served, and Claude publishes no heartbeat, so nothing else would move
149
- // ACC's lease while the session sits idle - which is when live delivery is
150
- // worth having.
151
- refreshBinding: leaseUntil => service.refreshDeliveryBinding({
152
- sessionId: session.sessionId, generation: session.generation, leaseUntil }),
153
- });
154
- await channel.listen();
155
- pump(channel.handleLine, () => { channel.close(); process.exit(0); });
156
- // Serve MCP immediately; a slow or failed ACC binding must not stop the
157
- // client connecting. The listening endpoint lets an in-flight hook finish
158
- // before this owner acquires the same lifecycle lock and completes startup.
159
- await activateClaudeChannel({ session, service, runtimeDir: paths.root, dataHome,
160
- env: process.env }).catch(() => {});
161
- return channel;
26
+ if (message.id === undefined || message.id === null) return null;
27
+ const result = answer(message);
28
+ return result === undefined
29
+ ? { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `unknown method: ${message.method}` } }
30
+ : { jsonrpc: "2.0", id: message.id, result };
162
31
  }
163
32
 
164
33
  export async function main() {
165
- await compose().catch(() => startInertChannel());
34
+ let buffer = "";
35
+ process.stdin.setEncoding("utf8");
36
+ process.stdin.on("data", chunk => {
37
+ buffer += chunk;
38
+ let newline = buffer.indexOf("\n");
39
+ while (newline !== -1) {
40
+ const line = buffer.slice(0, newline).trim();
41
+ buffer = buffer.slice(newline + 1);
42
+ newline = buffer.indexOf("\n");
43
+ const reply = line === "" ? null : handle(line);
44
+ if (reply !== null) process.stdout.write(`${JSON.stringify(reply)}\n`);
45
+ }
46
+ });
47
+ await new Promise(resolve => {
48
+ process.stdin.once("end", resolve);
49
+ process.once("SIGTERM", resolve);
50
+ process.once("SIGINT", resolve);
51
+ process.stdin.resume();
52
+ });
53
+ // A signal can arrive while Claude still holds the pipe open; a stdin left
54
+ // reading would keep this process alive after main() returns.
55
+ process.stdin.destroy();
166
56
  }
@@ -54,7 +54,6 @@ await serve({
54
54
  context: {
55
55
  service,
56
56
  deliveryRouter: createDeliveryRouter({ service, adapters, clock,
57
- platform: `${process.platform}-${process.arch}`,
58
57
  readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
59
58
  adapterId: adapter.id }) }),
60
59
  workspaceId: descriptor.id,
@@ -25,7 +25,6 @@ const runtime = {
25
25
  ids: { next: kind => createId(kind, randomBytes) },
26
26
  createDeliveryRouter: ({ service, clock, dataHome }) =>
27
27
  createDeliveryRouter({ service, adapters, clock,
28
- platform: `${process.platform}-${process.arch}`,
29
28
  readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
30
29
  adapterId: adapter.id }) }),
31
30
  // Asked only by `acc config init`, and only when stdout is a terminal. There
@@ -81,10 +81,15 @@ is not evidence for compaction. Counts add no ids to `offeredMessageIds` or
81
81
 
82
82
  `client.command` does double duty. `detect.mjs` uses it as the version-probe binary, and
83
83
  presence liveness separately walks the hook's process ancestry for the first ancestor whose
84
- executable basename matches it, to learn the client's own pid. Declare the binary the client
85
- actually runs as — `command: "claude"` for a client that really runs as `node` resolves
86
- nothing, and the failure is silent: the session gets `pid: null` and falls back to reading
87
- presence by age alone, with nothing telling you why.
84
+ executable basename matches it, to learn the client's own pid. A client shipped as a node
85
+ script, such as Gemini CLI, runs as `node`; for a `node` ancestor the walk also matches the
86
+ script it runs: the first argument after any interpreter options, by basename, with a
87
+ `.js`, `.mjs` or `.cjs` extension ignored. A word right after an option written without `=`
88
+ may be that option's value (`--require ./preload.cjs`), so it is also tried and the script
89
+ after it still counts. Declare the name the client actually runs as. A
90
+ command that matches neither resolves nothing, and the failure is silent: the session gets
91
+ `pid: null`, falls back to reading presence by age alone, and is not pinned to its
92
+ generation, with nothing telling you why.
88
93
 
89
94
  ## Declare only proven capabilities
90
95
 
@@ -148,35 +153,71 @@ to its capabilities:
148
153
 
149
154
  ```js
150
155
  nativeDelivery: {
151
- minimumByPlatform: { "darwin-arm64": "2.1.258" },
152
- anchors: [{ platform: "darwin-arm64", version: "2.1.258",
153
- protocolContract: "claude-code-channel-mcp-v1" }],
156
+ minimum: "2.1.282",
157
+ anchors: [{ version: "2.1.282", protocolContract: "claude-code-inbox-socket-v1" }],
154
158
  knownBad: [],
155
- activationKinds: ["shell-bootstrap"],
159
+ activationKinds: ["native-service"],
160
+ offerKind: "wake",
156
161
  }
157
162
  ```
158
163
 
159
164
  The rules `defineAdapter` enforces, and the ones the runtime applies:
160
165
 
161
- - A minimum is the **first passing capture** on that platform, never a guessed first vendor
162
- release. Every anchor must have passing `delivery.livePush` evidence for the same client,
163
- version, and platform, and each minimum must itself be an anchor.
164
- - There is intentionally **no maximum version**. A newer stable release is admitted only
165
- when a current read-only feature probe (`probeNativeDelivery()`) and a per-session
166
- handshake (`bindNativeSession()`) both report the anchored `protocolContract`;
167
- `evaluateNativeEligibility()` and `validateNativeHandshake()` are those two checks.
168
- - Prereleases require their own passing capture; they are `prerelease_not_captured` even
169
- when numerically newer. `knownBad` names exact versions or inclusive intervals.
170
- - Exact-version certification still governs every non-native capability;
166
+ - The minimum is the **first passing capture**, never a guessed first vendor release. Every
167
+ anchor must have passing `delivery.livePush` evidence for the same client and version, and
168
+ the minimum must itself be an anchor. The platform an evidence row names is where the
169
+ capture was taken; it is provenance, and the anchor names no platform.
170
+ - There is intentionally **no maximum version** and **no platform**. A newer release is
171
+ admitted, on every platform, only when a current read-only feature probe
172
+ (`probeNativeDelivery()`) and a per-session handshake (`bindNativeSession()`) both report
173
+ the anchored `protocolContract`; `evaluateNativeEligibility()` and
174
+ `validateNativeHandshake()` are those two checks.
175
+ - A prerelease is judged by its release triple, as hook capabilities are: `2.2.0-beta.1` is
176
+ `2.2.0` against the minimum and `knownBad`. `knownBad` names exact versions or inclusive
177
+ intervals. A version that cannot be read at all is `version_unavailable`.
178
+ - Certification evidence still governs every non-native capability;
171
179
  `effectiveCapabilities()` is unchanged. The native rule is used for live delivery alone.
180
+
181
+ What may refuse live delivery is exactly three things:
182
+
183
+ - the machine's own probe or per-session handshake failing;
184
+ - a client older than the first passing capture;
185
+ - a regression recorded in `knownBad`.
186
+
187
+ Nothing else: not the platform, not a prerelease suffix, not where the evidence was taken. A
188
+ transport that is technically different on some platform is the probe's fact to report, as
189
+ `native_delivery_unsupported`, never a gap in the captures. The 0.8.0 shape,
190
+ `minimumByPlatform` with a platform on each anchor, is refused by name; the design record is
191
+ `docs/design/2026-09-26-native-delivery-across-platforms.md` in the repository.
172
192
  - Native methods return closed facts (`validateNativeActivationPlan()` closes the
173
193
  activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
174
194
 
175
- `nativeDelivery.policySource` defaults to the session environment. An adapter may
176
- declare `"installation-record"` when hooks and senders must read current recorded
177
- recipient consent independently of the vendor process environment. Installation
178
- records the requested policy even when current activation is unavailable. An absent
179
- or invalid record means `off`; a previously published binding cannot override it.
195
+ `nativeDelivery.policySource` accepts only `"installation-record"`, which is also the
196
+ default. Hooks and senders read the current recorded recipient consent, independently of
197
+ the vendor process environment. Installation records the requested policy even when
198
+ current activation is unavailable. An absent or invalid record means `off`. A previously
199
+ published binding cannot override it.
200
+
201
+ `nativeDelivery.offerKind` says what an accepted offer puts in front of the model. It is
202
+ `"message"` (the default) or `"wake"`:
203
+
204
+ - `"message"` carries the body. The router records the receipt as `offered` with the
205
+ transport name and returns the outcome `offered`.
206
+ - `"wake"` carries only a notice that makes the client run a turn. The router records no
207
+ offer and returns the outcome `woken`. The receipt stays `queued`. The next-turn hook
208
+ records `offered` via `next-turn` after its output carried the body. Use `"wake"` only
209
+ where the turn that the wake starts runs the client's next-turn hook, as Claude Code's
210
+ `UserPromptSubmit` does. When the client takes the wake but holds it for its user's
211
+ approval, return `pendingApproval: true` with the acceptance; the outcome carries it so
212
+ the sender is told the truth.
213
+
214
+ A failed offer of either kind is recorded as a failed offer and leaves the receipt queued.
215
+
216
+ `nativeDelivery.activationKinds` names the mechanisms the adapter may plan:
217
+ `"native-config"` or `"native-service"`. A `native-service` mechanism with
218
+ `preExisting: true` and no commands describes a service that the client itself runs, such
219
+ as the Claude Code inbox. A `shell-bootstrap` mechanism can no longer be planned. Install
220
+ records of one stay readable, so that `acc install` and `acc update` can retire it.
180
221
 
181
222
  The optional `refreshNativeSession()` method may revalidate an expired lease before
182
223
  an offer. It returns the same closed handshake shape as `bindNativeSession()` and
@@ -204,7 +245,7 @@ earlier failures without duplicating a certification tuple.
204
245
 
205
246
  Installed hook wiring may reach tier 2, but each effective hook capability still
206
247
  requires exact client/version/platform evidence. Tier 3 has separate native
207
- contracts: Claude Code Channel from 2.1.258 and Codex LocalDaemon from 0.152.1 on
248
+ contracts: the Claude Code inbox wake from 2.1.282 and Codex LocalDaemon from 0.152.1 on
208
249
  macOS arm64, with current probes and exact session handshakes. Codex's earlier
209
250
  remote-wrapper failure remains historical evidence. Ordinary launch now preserves
210
251
  workspace identity; the full installed product matrix, including negative controls
@@ -220,9 +261,18 @@ return normalizedEvent({
220
261
  kind, sessionId, cwd, model, parentSessionId, tool,
221
262
  targets, // paths this call would WRITE. For a shell call, pass the command to
222
263
  // shellWriteTargets() — it reads write positions only, never reads.
264
+ permissionMode, // the client's own name for the session's permission mode, or null
265
+ endReason, // on sessionEnd, the client's own word for why, or null
223
266
  });
224
267
  ```
225
268
 
269
+ `permissionMode` and `endReason` are optional and default to `null`. Pass `permissionMode`
270
+ when the client's hook reports one: a native binding reads it to tell a sender whether the
271
+ client will hold a wake for approval. Claude Code reports it on most hooks but not on
272
+ `SessionStart`. Pass `endReason` on `sessionEnd` when the client says why, as Claude Code's
273
+ `reason` does (`"clear"` after `/clear`). ACC keeps it in the closed session record's
274
+ `extensions`, and a sender is told that the conversation was cleared.
275
+
226
276
  Refuse an unrecognised payload. Inventing a session attaches the wrong one, or a new one
227
277
  every hook, and looks like it is working.
228
278
 
@@ -48,6 +48,11 @@ The order is part of the public guarantee:
48
48
  4. ask one adapter to cross its transport boundary;
49
49
  5. only after acceptance, commit `offered` and an immutable success event.
50
50
 
51
+ An adapter whose contract declares `offerKind: "wake"` puts only a notice in front of the
52
+ client, never the body. The router records no offer for an accepted wake and returns the
53
+ outcome `woken`. The receipt stays `queued` until the recipient's next-turn hook output
54
+ carried the body. That hook then commits `offered` via `next-turn`.
55
+
51
56
  A failed offer records the selected recipient session id and generation with a safe
52
57
  diagnostic code, then leaves the receipt queued. Core validates that target provenance and
53
58
  derives the event actor from it. A crash after bytes cross the boundary but before the
@@ -78,12 +83,16 @@ or projected into agent context; session restart replaces it and SessionEnd atte
78
83
  cleanup. Doctor ignores closed or superseded generations even if cleanup could not finish. Diagnostic disk I/O runs in an
79
84
  unreferenced worker, so a stalled write cannot hold the hook process open.
80
85
 
81
- Claude's MCP process can become ready after the bounded SessionStart handshake
82
- has ended. Once its endpoint listens, the Channel completes the same validated
83
- binding without a user prompt. It acquires the session lifecycle lock, recovers
84
- any committed journal, and rechecks its exact owner and current consent before
85
- publication. The optional diagnostic records `channelReady`; MCP remains responsive
86
- if registration fails. This does not create a session or revive a closed one.
86
+ Claude Code's inbox binding runs inside ACC's own hooks, with no ACC process in the
87
+ session. Each turn hook reads the session's inbox socket from `CLAUDE_CODE_MESSAGING_SOCKET`
88
+ and verifies it against Claude Code's session registry `<config>/sessions/<pid>.json`. The
89
+ pid and the socket must match; the session id is checked from the first offer on, because
90
+ Claude Code rewrites the entry for a `/resume` only after SessionStart. The hook then writes
91
+ a private endpoint record and publishes the binding with a 120-second lease. The router refreshes an expired
92
+ lease with the same registry check, so an idle session stays reachable between turns. The
93
+ adapter repeats the check before each offer. A registry that disappeared, or that names
94
+ another session or socket, refuses the offer, so a wake never reaches a reused pid. This
95
+ does not create a session or revive a closed one.
87
96
 
88
97
  ## Certified capability versus current reachability
89
98
 
@@ -96,14 +105,16 @@ a third certification check. A binding says what that current generation
96
105
  exposes and whether its lease is current, while recipient policy says whether it may spend a
97
106
  turn.
98
107
 
99
- Claude Code Channel has installed-client evidence on 2.1.258 and 2.1.260; Codex
100
- LocalDaemon has it on 0.152.1 and 0.153.4; Antigravity CLI has it on 1.2.7 and later through a relay
101
- the agent starts in its own shell. All three use a captured platform minimum,
102
- current probe and exact session handshake. Codex preserves the ordinary client
103
- launch and verifies its registered thread and workspace; native delivery owns no vendor
104
- daemon lifecycle. Gemini CLI and Kimi Code have exact-version next-turn evidence only; Grok
105
- and generic MCP use inbox polling. Antigravity CLI additionally attaches
106
- a session only where the client reports an open workspace.
108
+ The Claude Code inbox wake has installed-client evidence on 2.1.282. Codex LocalDaemon
109
+ has it on 0.152.1 and 0.153.4. Antigravity CLI has it on 1.2.7 and later through a relay
110
+ the agent starts in its own shell. All three use a captured minimum that applies on every
111
+ platform, a current probe and an exact session handshake; a prerelease is judged by its
112
+ release triple. Claude Code and Codex preserve the ordinary
113
+ client launch. Claude Code verifies its session registry, and Codex verifies its registered
114
+ thread and workspace. Native delivery owns no vendor daemon lifecycle. Gemini CLI and Kimi
115
+ Code have exact-version next-turn evidence only. Grok and generic MCP use inbox polling.
116
+ Antigravity CLI additionally attaches a session only where the client reports an open
117
+ workspace.
107
118
 
108
119
  ## Storage and workspace identity
109
120
 
@@ -216,10 +227,12 @@ modes. It preserves explicit declarations and does not rewrite live leases. Miss
216
227
  changed generation files and unknown native bindings remain unverified holds.
217
228
  A session is also pinned to the generation
218
229
  it started with: its hooks delegate to that generation while the pin declares the same
219
- store contract this one does, and run the active generation otherwise. A generation
220
- directory is reclaimed once no control pointer, live lease, live pin or staging hold names
221
- it. Parsed help, version, and update recovery remain available when workspace admission is
222
- unavailable.
230
+ store contract this one does, and run the active generation otherwise. Apart from
231
+ `SessionEnd`, only the client's confirmed death releases a pin, so a session whose client
232
+ process cannot be found is not pinned, and a pin that names no client is reaped. A
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.
223
236
 
224
237
  An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
225
238
  stable package identity and the discovered integrity, and health-checks the staged runtime.