agents-can-communicate 0.5.7 → 0.5.9
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.
- package/README.md +99 -161
- package/bin/entrypoints/acc-claude-channel.mjs +8 -1
- package/bin/entrypoints/claude-channel-binding.mjs +46 -0
- package/docs/ARCHITECTURE.md +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +9 -6
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +9 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/own-version.mjs +11 -3
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +6 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +6 -1
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,186 +1,124 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**
|
|
1
|
+
# Agents Can Communicate (ACC)
|
|
2
|
+
|
|
3
|
+
**Let your AI coding sessions talk to each other.**
|
|
4
|
+
|
|
5
|
+
ACC connects independent AI coding sessions in the clients you already use. They can ask each
|
|
6
|
+
other questions, exchange reviews, and leave handoffs for another session to continue.
|
|
7
|
+
|
|
8
|
+
You open each client normally and choose its work. Every session keeps its own model,
|
|
9
|
+
conversation, and permissions. Coordination runs locally, with no lead agent managing the
|
|
10
|
+
others.
|
|
11
|
+
|
|
12
|
+
```mermaid
|
|
13
|
+
flowchart TB
|
|
14
|
+
you["You choose the tools and work"]
|
|
15
|
+
codex["Codex<br/>Feature A"]
|
|
16
|
+
claude["Claude Code<br/>Feature B"]
|
|
17
|
+
gemini["Gemini CLI<br/>Feature C"]
|
|
18
|
+
acc["Communication via ACC"]
|
|
19
|
+
|
|
20
|
+
you -.-> codex
|
|
21
|
+
you -.-> claude
|
|
22
|
+
you -.-> gemini
|
|
23
|
+
codex <--> acc
|
|
24
|
+
claude <--> acc
|
|
25
|
+
gemini <--> acc
|
|
26
|
+
|
|
27
|
+
subgraph handoff["When you switch models"]
|
|
28
|
+
direction LR
|
|
29
|
+
current["Current<br/>session"] --> saved["Handoff<br/>Done · Decisions<br/>Next steps"]
|
|
30
|
+
saved --> next["Next<br/>session"]
|
|
31
|
+
end
|
|
32
|
+
acc ~~~ handoff
|
|
33
|
+
|
|
34
|
+
classDef human fill:#f1f5f9,stroke:#8593a3,color:#202a35
|
|
35
|
+
classDef session fill:#fff,stroke:#ced4d9,color:#202a35
|
|
36
|
+
classDef shared fill:#e8edff,stroke:#385cde,color:#202a35
|
|
37
|
+
class you human
|
|
38
|
+
class codex,claude,gemini,current,next session
|
|
39
|
+
class acc,saved shared
|
|
40
|
+
style handoff fill:transparent,stroke:#ced4d9
|
|
41
|
+
linkStyle 3,4,5,6,7 stroke:#385cde,stroke-width:2px
|
|
42
|
+
```
|
|
4
43
|
|
|
5
|
-
|
|
6
|
-
and models you prefer.
|
|
44
|
+
## When ACC helps
|
|
7
45
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
46
|
+
- **Switch models mid-feature.** When a limit approaches or you want another model’s approach,
|
|
47
|
+
leave a handoff with decisions and unfinished work for the next session.
|
|
48
|
+
- **Get a second opinion.** Ask another session to review a specific change and send its
|
|
49
|
+
findings directly to the implementing agent.
|
|
50
|
+
- **Bring parallel features together.** Let sessions working on frontend and backend ask each
|
|
51
|
+
other about a shared API before building around different assumptions.
|
|
12
52
|
|
|
13
|
-
|
|
14
|
-
the same workspace on the same machine and operating-system user; Git is optional.
|
|
53
|
+
## Try one handoff
|
|
15
54
|
|
|
16
|
-
|
|
55
|
+
You’ll need **macOS or Linux, Node.js 24 or newer**, and supported coding clients on the same
|
|
56
|
+
machine and operating-system user.
|
|
17
57
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
58
|
+
```bash
|
|
59
|
+
npm install -g agents-can-communicate
|
|
60
|
+
acc install
|
|
61
|
+
```
|
|
21
62
|
|
|
22
|
-
|
|
63
|
+
The installer connects supported clients it finds. Follow its activation instructions, review
|
|
64
|
+
any required hook or plugin trust, then restart your clients from the project directory. The
|
|
65
|
+
[setup guide](docs/GETTING_STARTED.md) covers client-specific steps.
|
|
23
66
|
|
|
24
|
-
|
|
67
|
+
For example, when pausing work on an account-registration feature, ask:
|
|
25
68
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
```text
|
|
70
|
+
Save a partial handoff in ACC for the registration feature. Include
|
|
71
|
+
what is done, our decisions, what remains, and what you actually
|
|
72
|
+
verified. I will continue in another session.
|
|
73
|
+
```
|
|
30
74
|
|
|
31
|
-
|
|
32
|
-
others are doing. As they work, they can notice a dependency and coordinate. An
|
|
33
|
-
illustrative exchange:
|
|
75
|
+
Open another supported client in the same project and ask:
|
|
34
76
|
|
|
35
77
|
```text
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
Continue the registration feature from its ACC handoff. Check the
|
|
79
|
+
saved decisions against the current files, then take the next
|
|
80
|
+
unfinished step. Ask me if the scope is unclear.
|
|
39
81
|
```
|
|
40
82
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
every task. [Client support](#when-messages-arrive) determines how automatically that
|
|
44
|
-
awareness reaches it.
|
|
83
|
+
The next session should identify the saved decision and begin the remaining work. It can find
|
|
84
|
+
the handoff even if it was opened after the previous session stopped.
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
work, remaining work, and blockers. A later session can look up that history. Git commits
|
|
49
|
-
can identify a revision; named files and versions work when Git is unavailable.
|
|
50
|
-
See [requests and replies](docs/CLI.md#messages-and-requests) and
|
|
51
|
-
[handoffs](docs/CLI.md#handoff).
|
|
86
|
+
Save the handoff while the first model can still respond. ACC preserves explicitly recorded
|
|
87
|
+
context; it cannot recover details that were never saved.
|
|
52
88
|
|
|
53
|
-
##
|
|
89
|
+
## Client support
|
|
54
90
|
|
|
55
|
-
|
|
56
|
-
|
|
91
|
+
Integrations are available for **Claude Code, Codex, Gemini CLI, Grok, and Kimi Code**. Other
|
|
92
|
+
clients can connect through [MCP](docs/MCP.md) with their own configuration and coordination
|
|
93
|
+
instructions.
|
|
57
94
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
acc install
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The installer connects the supported clients it finds. Follow its activation instructions,
|
|
64
|
-
then restart your clients from the project directory. In Codex, check `/plugins` and
|
|
65
|
-
review the current ACC definitions in `/hooks`; changed hooks may need fresh trust.
|
|
66
|
-
[Getting started](docs/GETTING_STARTED.md) covers activation and preserved sandbox settings.
|
|
67
|
-
|
|
68
|
-
For Codex live delivery on Apple Silicon macOS, installation also configures outgoing
|
|
69
|
-
local socket access on Codex 0.153.4 or newer when using default workspace permissions.
|
|
70
|
-
Custom policies are preserved. One default-No choice covers all selected clients that need
|
|
71
|
-
live-delivery consent. On Codex 0.154.0 or newer, the choice includes downloading a missing
|
|
72
|
-
standalone package from OpenAI and starting its local service. ACC installs the matching
|
|
73
|
-
version and preserves your existing `codex` command and shell profiles. The client still owns session
|
|
74
|
-
startup, hook trust, and Claude Channels approval. Doctor reports service infrastructure,
|
|
75
|
-
session binding, and outgoing permissions separately.
|
|
76
|
-
|
|
77
|
-
Open two sessions and give them ordinary tasks, as above. Look for an agent discovering a
|
|
78
|
-
peer, checking who is changing a file, asking about a shared dependency, or replying to a
|
|
79
|
-
review request.
|
|
80
|
-
|
|
81
|
-
Run `acc doctor` from the project if a peer is missing. A directory containing ACC's own
|
|
82
|
-
state, commonly your home directory, cannot be used as a workspace; start the client in a
|
|
83
|
-
project directory. See [Troubleshooting](docs/TROUBLESHOOTING.md).
|
|
84
|
-
|
|
85
|
-
Already using ACC? Follow the [upgrade guide](docs/UPGRADING.md), including the 0.4.x →
|
|
86
|
-
0.5.0 update and the data-format boundary when moving from 0.3.1.
|
|
87
|
-
|
|
88
|
-
## When messages arrive
|
|
89
|
-
|
|
90
|
-
Messages are saved locally before delivery is attempted. A durable inbox remains
|
|
91
|
-
available when a faster route cannot be used.
|
|
92
|
-
|
|
93
|
-
| Client | How the agent receives a message |
|
|
94
|
-
|---|---|
|
|
95
|
-
| Codex CLI, Claude Code, Gemini CLI, Kimi Code | At the next normal turn on the exact verified versions and platforms; otherwise through explicit ACC inbox reads. |
|
|
96
|
-
| Grok | Through explicit ACC inbox reads, using its installed hooks and skill. CLI ownership was verified on Grok 1.0.24. |
|
|
97
|
-
| Other clients connected through [MCP](docs/MCP.md) | Through ACC tools and inbox reads. Generic MCP requires its own client configuration and coordination instructions. |
|
|
98
|
-
|
|
99
|
-
Grok's updated skill first runs public status through the terminal. The ACC hook reminder
|
|
100
|
-
after that result supplies the session's own CLI arguments for subsequent inbox reads and
|
|
101
|
-
mutations. It adds no automatic peer-message injection or idle delivery. A relocated
|
|
102
|
-
`GROK_HOME` is respected by install, doctor, and uninstall.
|
|
103
|
-
|
|
104
|
-
**Optional live delivery can start a turn in an idle Codex or Claude Code session.** It is
|
|
105
|
-
experimental, off by default, and can spend model tokens. On Apple Silicon Macs, Codex
|
|
106
|
-
0.152.1 or newer requires LocalDaemon infrastructure and a verified session;
|
|
107
|
-
Claude Code 2.1.258 or newer requires zsh and client-side Channels activation; check its
|
|
108
|
-
startup notice for ACC and accept the development warning when shown. An MCP connection
|
|
109
|
-
alone does not verify inbound delivery. Messages arriving mid-turn wait for the turn to
|
|
110
|
-
finish. The receiving session's
|
|
111
|
-
opt-in policy and current reachability determine whether delivery can proceed.
|
|
112
|
-
`acc install` reports each client's delivery state and can save Codex consent before its
|
|
113
|
-
service is available. Use `--delivery actionable|all` for explicit automation and
|
|
114
|
-
`--delivery off` to disable incoming automatic requests. `acc doctor` also names each session’s last native binding result,
|
|
115
|
-
including missing launch consent, an unidentified client process or a failed handshake.
|
|
116
|
-
It distinguishes a disabled policy from an unavailable service or a missing live channel
|
|
117
|
-
in the current project.
|
|
118
|
-
|
|
119
|
-
A Codex thread retained by LocalDaemon can receive opted-in messages after its terminal
|
|
120
|
-
exits. Turning ACC delivery off prevents new native offers; already accepted queue entries
|
|
121
|
-
remain with the client. [Compatibility and delivery controls](docs/CAPABILITIES.md) describe
|
|
122
|
-
the exact evidence, versions, platforms, and fallback paths.
|
|
123
|
-
|
|
124
|
-
A recorded message is send success. An offer is not proof of reading, and an
|
|
125
|
-
acknowledgement or reply is not proof that the requested work is complete.
|
|
126
|
-
|
|
127
|
-
## Keep the workflow you like
|
|
128
|
-
|
|
129
|
-
- **Your usual tools.** Start clients with their normal commands. You choose each agent's
|
|
130
|
-
task, model, and permissions.
|
|
131
|
-
- **Separate checkouts, one project.** Git worktrees share an ACC workspace. Plain folders
|
|
132
|
-
work too; optional workspace configuration can supply a shared identity and roots.
|
|
133
|
-
- **Agree before editing.** Agents can claim files and identify overlapping work. CLI claims
|
|
134
|
-
default to advisory. Guarded claims require certified guards from every live participant.
|
|
135
|
-
[How claims work](docs/CONCEPTS.md#intent-is-awareness-a-claim-commits).
|
|
136
|
-
- **Focused context and durable history.** Normal turn context is bounded. Inbox and history
|
|
137
|
-
return summary pages, with message bodies fetched by id. Agents can supersede or withdraw
|
|
138
|
-
old decisions and recover prior handoffs when needed.
|
|
139
|
-
- **Local coordination.** State lives in app data outside your project. ACC never collects
|
|
140
|
-
or shares raw transcripts; peer messages are untrusted input. Your clients keep using
|
|
141
|
-
their usual model providers.
|
|
142
|
-
|
|
143
|
-
One npm package. ACC needs no separate account, model API key, or hosted service.
|
|
144
|
-
|
|
145
|
-
## Update or remove ACC
|
|
146
|
-
|
|
147
|
-
Initial installation enables automatic updates. ACC downloads stable releases in the
|
|
148
|
-
background, then switches the runtime and refreshes integrations once nothing blocks. A
|
|
149
|
-
live client or ACC process blocks only while the store contract it declares differs from
|
|
150
|
-
the incoming version's or is unknown; one declaring a matching contract keeps running
|
|
151
|
-
through the switch. When a verified Codex service needs a restart,
|
|
152
|
-
`acc update` asks once, then completes maintenance in a separate process. Open clients
|
|
153
|
-
disconnect; `acc doctor` reports progress and the result. See [update and recovery details](docs/UPGRADING.md#confirmed-client-service-maintenance).
|
|
154
|
-
Restart or resume clients afterward and complete any requested hook or plugin trust review.
|
|
155
|
-
|
|
156
|
-
For an immediate update:
|
|
95
|
+
Automatic delivery depends on the client version and platform. Verified integrations can
|
|
96
|
+
provide messages at the next normal turn; other sessions read their ACC inbox explicitly.
|
|
157
97
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
98
|
+
Experimental live delivery can wake eligible Claude Code and Codex sessions on Apple Silicon
|
|
99
|
+
macOS. It requires opt-in and an active verified connection, is off by default, and can spend
|
|
100
|
+
model tokens. Messages for busy sessions queue until the current turn ends.
|
|
161
101
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`acc update --auto on` once: that version's uninstall record lost the previous preference.
|
|
102
|
+
See [client capabilities](docs/CAPABILITIES.md) for exact support. Run `acc doctor` from your
|
|
103
|
+
project if a peer is missing or delivery differs from what you expect; see
|
|
104
|
+
[troubleshooting](docs/TROUBLESHOOTING.md). Agents decide when to coordinate; ACC does not
|
|
105
|
+
guarantee they will notice every dependency.
|
|
167
106
|
|
|
168
|
-
|
|
107
|
+
## Local and independent
|
|
169
108
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
109
|
+
Sessions share coordination within the same ACC workspace. Git is optional; worktrees of one
|
|
110
|
+
repository share that workspace while keeping separate files. Messages and handoffs are stored
|
|
111
|
+
in local app data outside your project. ACC never collects or shares raw session transcripts.
|
|
173
112
|
|
|
174
|
-
ACC
|
|
175
|
-
|
|
176
|
-
preference.
|
|
113
|
+
ACC needs no separate account, model API key, or hosted service. Your coding clients keep using
|
|
114
|
+
their existing provider access.
|
|
177
115
|
|
|
178
|
-
|
|
116
|
+
Automatic updates are enabled on first install. Use `acc update --auto off` to disable them,
|
|
117
|
+
`acc update` to update manually, and `acc uninstall` to remove integrations. See
|
|
118
|
+
[update controls](docs/UPGRADING.md) for details.
|
|
179
119
|
|
|
180
|
-
|
|
181
|
-
[
|
|
182
|
-
[Security](docs/SECURITY_MODEL.md)
|
|
120
|
+
MIT-licensed and permanently noncommercial. Try it on one real task and
|
|
121
|
+
[tell us where you still had to carry messages yourself](https://github.com/automatis-tools/agents-can-communicate/issues).
|
|
183
122
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
and the [adapter guide](docs/ADAPTER_AUTHORING.md).
|
|
123
|
+
[Documentation](docs/index.md) ·
|
|
124
|
+
[Contributing](https://github.com/automatis-tools/agents-can-communicate/blob/main/AGENTS.md)
|
|
@@ -25,6 +25,7 @@ import { createAccChannel, endpointDir, routeAck, routeReply }
|
|
|
25
25
|
from "@agents-can-communicate/adapter-claude-code/channel";
|
|
26
26
|
|
|
27
27
|
import { pump, startInertChannel } from "./claude-channel-stdio.mjs";
|
|
28
|
+
import { activateClaudeChannel } from "./claude-channel-binding.mjs";
|
|
28
29
|
|
|
29
30
|
const clock = { now: () => new Date().toISOString() };
|
|
30
31
|
const ids = { next: kind => createId(kind, randomBytes) };
|
|
@@ -70,7 +71,8 @@ export async function resolveSession({ runtimeDir, service, env, ownClientPid,
|
|
|
70
71
|
const matched = mine.filter(binding => exported.has(binding.harnessSessionId));
|
|
71
72
|
const chosen = mine.length === 1 ? mine[0] : matched.length === 1 ? matched[0] : null;
|
|
72
73
|
return chosen === null ? null
|
|
73
|
-
: { sessionId: chosen.accSessionId, generation: chosen.generation, clientPid: chosen.clientPid
|
|
74
|
+
: { sessionId: chosen.accSessionId, generation: chosen.generation, clientPid: chosen.clientPid,
|
|
75
|
+
harnessSessionId: chosen.harnessSessionId };
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/** The client process this Channel was spawned by, or null when nobody knows. */
|
|
@@ -151,6 +153,11 @@ async function compose() {
|
|
|
151
153
|
});
|
|
152
154
|
await channel.listen();
|
|
153
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(() => {});
|
|
154
161
|
return channel;
|
|
155
162
|
}
|
|
156
163
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
2
|
+
import { loadSessionBinding, storeNativeAttempt } from "@agents-can-communicate/adapter-sdk";
|
|
3
|
+
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
4
|
+
import { establishNativeBinding, livePolicyFrom } from "@agents-can-communicate/hook-runner/native-binding";
|
|
5
|
+
import { withSessionLifecycle } from "@agents-can-communicate/hook-runner/session-lifecycle";
|
|
6
|
+
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
7
|
+
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
8
|
+
|
|
9
|
+
// SessionStart cannot wait for MCP indefinitely. The endpoint's owner completes
|
|
10
|
+
// the same handshake when it starts later, without a prompt or an LLM turn.
|
|
11
|
+
// Serialize with the hooks, then reread ownership: a queued startup must never
|
|
12
|
+
// publish for a replaced/closed session or undo SessionEnd's retirement.
|
|
13
|
+
export async function activateClaudeChannel({ session, service, runtimeDir, dataHome, env,
|
|
14
|
+
deadlineAt = Date.now() + 5_000 }) {
|
|
15
|
+
if (typeof session.harnessSessionId !== "string") return null;
|
|
16
|
+
return withSessionLifecycle({ root: runtimeDir, sessionId: session.harnessSessionId,
|
|
17
|
+
clock: service.clock, deadlineAt }, async () => {
|
|
18
|
+
// A prior hook can die after committing its journal. Recover that close
|
|
19
|
+
// before reading ownership, just as the hook runner does under this lock.
|
|
20
|
+
const { clock, ids } = service;
|
|
21
|
+
const store = await openFilesystemStore({ root: runtimeDir, clock, ids,
|
|
22
|
+
workspaceId: service.store.workspaceId, deadlineAt });
|
|
23
|
+
service = createCoordinationService({ store, clock, ids });
|
|
24
|
+
const current = await loadSessionBinding({ runtimeDir,
|
|
25
|
+
harnessSessionId: session.harnessSessionId });
|
|
26
|
+
if (current?.accSessionId !== session.sessionId || current.generation !== session.generation
|
|
27
|
+
|| current.clientPid !== session.clientPid) return null;
|
|
28
|
+
const located = await service.locateSession(session.sessionId);
|
|
29
|
+
if (located?.record.state !== "open" || located.record.generation !== session.generation) return null;
|
|
30
|
+
const policy = livePolicyFrom(env);
|
|
31
|
+
if (policy === "off" || await readInstalledLivePolicy({ dataHome,
|
|
32
|
+
adapterId: "claude_code" }) === "off") return null;
|
|
33
|
+
if (Date.now() >= deadlineAt) return null;
|
|
34
|
+
const result = await establishNativeBinding({ adapter: createClaudeCodeAdapter(),
|
|
35
|
+
event: { kind: "channelReady", sessionId: session.harnessSessionId },
|
|
36
|
+
hookBinding: current, clientVersion: current.clientVersion, platform: current.platform,
|
|
37
|
+
livePolicy: policy, service, runtimeDir, clock: service.clock, env,
|
|
38
|
+
timeoutMs: Math.max(1, Math.min(750, deadlineAt - Date.now())) });
|
|
39
|
+
await storeNativeAttempt({ runtimeDir, harnessSessionId: session.harnessSessionId,
|
|
40
|
+
accSessionId: session.sessionId, generation: session.generation, deadlineAt,
|
|
41
|
+
nativeAttempt: { at: service.clock.now(), event: "channelReady", state: result.state,
|
|
42
|
+
reasonCode: result.reasonCode, policy, policySource: "bootstrap-environment",
|
|
43
|
+
policyStatus: "enabled", clientProcess: "identified" } });
|
|
44
|
+
return result;
|
|
45
|
+
});
|
|
46
|
+
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -78,6 +78,13 @@ or projected into agent context; session restart replaces it and SessionEnd atte
|
|
|
78
78
|
cleanup. Doctor ignores closed or superseded generations even if cleanup could not finish. Diagnostic disk I/O runs in an
|
|
79
79
|
unreferenced worker, so a stalled write cannot hold the hook process open.
|
|
80
80
|
|
|
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.
|
|
87
|
+
|
|
81
88
|
## Certified capability versus current reachability
|
|
82
89
|
|
|
83
90
|
Ordinary hook capabilities say an exact client version on an exact platform passed a
|
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
|
|
10
10
|
import { acccreatedFile, bakeSkillCommand, blankJson, defaultBootstrap, defaultChannel,
|
|
11
11
|
mergeOwnedEntries, ownedEntries,
|
|
12
|
-
|
|
12
|
+
keepVersions, ownVersion, stampPluginVersion,
|
|
13
13
|
removeIfEmpty,
|
|
14
14
|
removeInstalledTree,
|
|
15
15
|
removeOwnedEntries, writeCliShim, writeForeignJson, writeHookShim }
|
|
@@ -181,7 +181,7 @@ async function layOutPlugin(target, { runner, node, cli, channel, live }) {
|
|
|
181
181
|
version: await pluginVersion(), io: { readFile, writeFile } });
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
export async function installClaudePlugin({ configDir, runner, cli,
|
|
184
|
+
export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null, node = process.execPath,
|
|
185
185
|
channel = defaultChannel(), livePolicy = "off", now = new Date() }) {
|
|
186
186
|
const live = livePolicy === "actionable" || livePolicy === "all";
|
|
187
187
|
// Everything this will merge into, read before a byte is written. A settings
|
|
@@ -206,10 +206,13 @@ export async function installClaudePlugin({ configDir, runner, cli, preserveVers
|
|
|
206
206
|
// run `claude plugin install`, exactly as the Codex adapter does, because the
|
|
207
207
|
// command's only effect is this copy plus the two registry entries below.
|
|
208
208
|
await layOutPlugin(cached, { runner, node, cli, channel, live });
|
|
209
|
-
//
|
|
210
|
-
// so every upgrade would otherwise leave the previous
|
|
211
|
-
// this one - invisible while the version never moved,
|
|
212
|
-
|
|
209
|
+
// The copy just written, plus the one an upgrade moved off. A client caches a
|
|
210
|
+
// plugin under its version, so every upgrade would otherwise leave the previous
|
|
211
|
+
// release's tree beside this one - invisible while the version never moved,
|
|
212
|
+
// three deep once it did. The one it moved off stays because a session open
|
|
213
|
+
// across the upgrade still runs hooks from the path it recorded at start;
|
|
214
|
+
// anything older than that holds no session and is litter.
|
|
215
|
+
await keepVersions({ root: path.dirname(cached), keep: [version, keepPreviousVersion],
|
|
213
216
|
io: { readdir, rm } });
|
|
214
217
|
|
|
215
218
|
await writeClientJson(knownMarketplacesPath(configDir), {
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
import { bakeSkillCommand, blankJson, blankText, removeIfEmpty, removeInstalledTree,
|
|
8
|
-
|
|
8
|
+
keepVersions, ownVersion, stampPluginVersion,
|
|
9
9
|
tomlString, writeCliShim, writeForeignJson, writeHookShim }
|
|
10
10
|
from "@agents-can-communicate/adapter-sdk";
|
|
11
11
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
@@ -160,7 +160,7 @@ const sandboxReview = (config, file, stateRoot) =>
|
|
|
160
160
|
: [];
|
|
161
161
|
|
|
162
162
|
export async function installCodexPlugin({ home, agentsHome = home,
|
|
163
|
-
codexHome = path.join(home, ".codex"), dataHome, stateRoot, runner, node, cli,
|
|
163
|
+
codexHome = path.join(home, ".codex"), dataHome, stateRoot, runner, node, cli, keepPreviousVersion = null,
|
|
164
164
|
requestedLivePolicy, livePolicy, clientVersion, platform }) {
|
|
165
165
|
// Read before writing, so a manifest that will not parse is found before a
|
|
166
166
|
// plugin tree is laid down that nothing will then be able to remove.
|
|
@@ -228,9 +228,13 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
228
228
|
const cached = cachedVersionPath(codexHome, version);
|
|
229
229
|
await rm(cached, { recursive: true, force: true });
|
|
230
230
|
await cp(target, cached, { recursive: true });
|
|
231
|
-
//
|
|
232
|
-
// directory. The marketplace cache root above it holds other
|
|
233
|
-
|
|
231
|
+
// The copy just written, plus the one an upgrade moved off - and only inside
|
|
232
|
+
// this plugin's own directory. The marketplace cache root above it holds other
|
|
233
|
+
// people's plugins. A session open across the upgrade still fires its hooks
|
|
234
|
+
// from the path it started with, so removing that one now would silence it
|
|
235
|
+
// until restart; anything older than it holds no session and is litter.
|
|
236
|
+
await keepVersions({ root: path.dirname(cached), keep: [version, keepPreviousVersion],
|
|
237
|
+
io: { readdir, rm } });
|
|
234
238
|
|
|
235
239
|
// The plugin's own directory in the cache. Not the versioned one inside it,
|
|
236
240
|
// which goes stale the moment the version changes - and not the marketplace
|
|
@@ -15,7 +15,7 @@ export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, write
|
|
|
15
15
|
export { projectContext, projectContextResult } from "./context-projector.mjs";
|
|
16
16
|
export { decisionBody, decisionLines } from "./decision-text.mjs";
|
|
17
17
|
export { shellWriteTargets } from "./shell-writes.mjs";
|
|
18
|
-
export {
|
|
18
|
+
export { keepVersions, ownVersion, stampPluginVersion } from "./own-version.mjs";
|
|
19
19
|
export { editJson, readJson } from "./json-text.mjs";
|
|
20
20
|
export { formatJsonAs, jsonStyleOf, mergeOwnedConfig, mergeOwnedEntries, ownedEntries, ownedKeys,
|
|
21
21
|
acccreatedFile, removeIfEmpty, removeOwnedConfig, removeOwnedEntries, writeForeignJson,
|
|
@@ -11,7 +11,7 @@ import { NATIVE_REASON_CODES, TIMESTAMP } from "./native-vocabulary.mjs";
|
|
|
11
11
|
export function nativeAttemptFrom(value) {
|
|
12
12
|
if (!value || typeof value.at !== "string" || !TIMESTAMP.test(value.at)
|
|
13
13
|
|| !Number.isFinite(Date.parse(value.at))
|
|
14
|
-
|| !["sessionStart", "beforeTurn"].includes(value.event)
|
|
14
|
+
|| !["sessionStart", "beforeTurn", "channelReady"].includes(value.event)
|
|
15
15
|
|| !["active", "off", "degraded", "unsupported"].includes(value.state)
|
|
16
16
|
|| !(value.reasonCode === null || NATIVE_REASON_CODES.includes(value.reasonCode))
|
|
17
17
|
|| !["off", "actionable", "all"].includes(value.policy)
|
|
@@ -57,23 +57,31 @@ export async function stampPluginVersion({ file, version, io }) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Leave
|
|
60
|
+
* Leave the named copies of a versioned plugin and remove the rest.
|
|
61
61
|
*
|
|
62
62
|
* These clients cache a plugin under its version. Until the version tracked the
|
|
63
63
|
* package it never changed, every install landed in the same directory and
|
|
64
64
|
* overwrote itself, and nothing accumulated. Once it started moving, the first
|
|
65
65
|
* upgrade left three copies of ACC in a home that should hold one.
|
|
66
66
|
*
|
|
67
|
+
* `keep` is a list rather than the single version just written, because these
|
|
68
|
+
* clients pin one `installPath` per plugin and a session reads it once. A
|
|
69
|
+
* session already open when an upgrade lands still holds the path it started
|
|
70
|
+
* with, so an upgrade names the version it moved off as well as the one it
|
|
71
|
+
* wrote, and that session keeps firing hooks until it is restarted. An install
|
|
72
|
+
* with nothing to hold passes a null, which is dropped here.
|
|
73
|
+
*
|
|
67
74
|
* Scoped to the plugin's own directory. The marketplace cache root above it
|
|
68
75
|
* holds every plugin installed from that marketplace, and removing that root
|
|
69
76
|
* once took a plugin the user had installed themselves - so a sibling here is
|
|
70
77
|
* an older ACC, and a sibling one level up is somebody else's.
|
|
71
78
|
*/
|
|
72
|
-
export async function
|
|
79
|
+
export async function keepVersions({ root, keep, io }) {
|
|
80
|
+
const wanted = new Set(keep.filter(version => typeof version === "string" && version !== ""));
|
|
73
81
|
const entries = await io.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
74
82
|
const removed = [];
|
|
75
83
|
for (const entry of entries) {
|
|
76
|
-
if (!entry.isDirectory() || entry.name
|
|
84
|
+
if (!entry.isDirectory() || wanted.has(entry.name)) continue;
|
|
77
85
|
await io.rm(path.join(root, entry.name), { recursive: true, force: true });
|
|
78
86
|
removed.push(entry.name);
|
|
79
87
|
}
|
|
@@ -56,7 +56,12 @@ export async function installManaged({ packageRoot, managerRoot, dataHome, home,
|
|
|
56
56
|
targets: [...new Set([...(previous?.targets ?? []), ...targets])], notice: null };
|
|
57
57
|
await writeControl(root, control);
|
|
58
58
|
const paths = await writeLaunchers(root, runtime.root);
|
|
59
|
-
|
|
59
|
+
// The guard above admits only an install from the active generation, so
|
|
60
|
+
// this names the version being written and the adapters keep one copy.
|
|
61
|
+
// It is read from the pointer rather than assumed, so a future relaxation
|
|
62
|
+
// of that guard keeps the outgoing copy instead of silently removing it.
|
|
63
|
+
const result = await apply({ ...paths,
|
|
64
|
+
keepPreviousVersion: previous?.active.version ?? null });
|
|
60
65
|
// Service readiness is separate from integration installation. Its failure
|
|
61
66
|
// must not fence a fully installed runtime or make the durable inbox unusable.
|
|
62
67
|
const integrationFailed = result.failed.some(failure => failure.stage !== "service-setup"
|
|
@@ -12,8 +12,13 @@ export async function prepareRefresh({ control, root, env = process.env, callerP
|
|
|
12
12
|
const wanted = new Set(control.targets);
|
|
13
13
|
const adapters = ALL_ADAPTERS().filter(adapter => wanted.has(adapter.id));
|
|
14
14
|
if (adapters.length !== wanted.size) throw new Error("candidate cannot refresh every installed integration");
|
|
15
|
+
// The version this refresh is moving off. Clients that cache a plugin under
|
|
16
|
+
// its version record one path per plugin and read it once per session, so a
|
|
17
|
+
// session open across the update keeps running from the outgoing copy until it
|
|
18
|
+
// restarts. Naming it here keeps that copy; everything older than it goes.
|
|
15
19
|
const context = { ...clientContext(control.home, path.join(dataHome, "acc"),
|
|
16
|
-
{ env, shell: shellOf(env), dataHome }), ...stablePaths(root),
|
|
20
|
+
{ env, shell: shellOf(env), dataHome }), ...stablePaths(root),
|
|
21
|
+
keepPreviousVersion: control.active.version };
|
|
17
22
|
const recorded = (await loadOwnership({ dataHome })).installs;
|
|
18
23
|
const detected = await detectInstallation({ adapters, context, probeTimeoutMs: probeTimeout(env) });
|
|
19
24
|
const deliveryByAdapter = Object.fromEntries(adapters.map(adapter => [adapter.id,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/hook-runner",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/runner.mjs",
|
|
8
8
|
"./client-pid": "./src/client-pid.mjs",
|
|
9
|
-
"./process-table": "./src/process-table.mjs"
|
|
9
|
+
"./process-table": "./src/process-table.mjs",
|
|
10
|
+
"./native-binding": "./src/native-binding.mjs",
|
|
11
|
+
"./session-lifecycle": "./src/session-lifecycle.mjs"
|
|
10
12
|
},
|
|
11
13
|
"files": [
|
|
12
14
|
"src/"
|