agents-can-communicate 0.7.1 → 0.8.0
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 +3 -1
- package/bin/entrypoints/acc-bootstrap.mjs +6 -48
- package/bin/entrypoints/acc-claude-channel.mjs +47 -157
- package/docs/ADAPTER_AUTHORING.md +41 -10
- package/docs/ARCHITECTURE.md +23 -13
- package/docs/CAPABILITIES.md +19 -16
- package/docs/CLI.md +48 -13
- package/docs/CONCEPTS.md +5 -4
- package/docs/CONFIGURATION.md +18 -11
- package/docs/GETTING_STARTED.md +15 -12
- package/docs/GLOSSARY.md +2 -1
- package/docs/HOW_IT_WORKS.md +8 -4
- package/docs/PROTOCOL.md +25 -6
- package/docs/RELEASING.md +3 -2
- package/docs/SECURITY_MODEL.md +14 -1
- package/docs/TROUBLESHOOTING.md +64 -27
- package/docs/UPGRADING.md +49 -8
- package/docs/WHY_ACC.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +25 -30
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +254 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- 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/events.mjs +16 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +23 -11
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +5 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +25 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +19 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +24 -7
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +24 -11
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +3 -7
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
- 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/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
- package/package.json +1 -1
- package/bin/acc-bootstrap.mjs +0 -7
- package/bin/acc-claude-channel.mjs +0 -10
- package/bin/entrypoints/claude-channel-binding.mjs +0 -46
- package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
|
@@ -7,24 +7,13 @@ import { canonicalManagerRoot, readControl, readManagedJson } from "./state.mjs"
|
|
|
7
7
|
import { commandPrefix } from "./command-prefix.mjs";
|
|
8
8
|
|
|
9
9
|
export const ENTRY_KINDS = Object.freeze([
|
|
10
|
-
"acc", "acc-hook", "acc-mcp", "acc-
|
|
10
|
+
"acc", "acc-hook", "acc-mcp", "acc-antigravity-relay",
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export function parseBootstrapOptions(args) {
|
|
18
|
-
const options = {};
|
|
19
|
-
for (let index = 0; index < args.length; index += 2) {
|
|
20
|
-
const key = BOOTSTRAP_FLAGS.get(args[index]);
|
|
21
|
-
const value = args[index + 1];
|
|
22
|
-
if (key === undefined || typeof value !== "string" || value === "" || key in options) return null;
|
|
23
|
-
options[key] = value;
|
|
24
|
-
}
|
|
25
|
-
return BOOTSTRAP_FLAGS.values().every(key => key in options) && path.isAbsolute(options.dataHome)
|
|
26
|
-
? options : null;
|
|
27
|
-
}
|
|
13
|
+
// Kinds a 0.7.x install wrote a launcher for. The package still ships their
|
|
14
|
+
// entrypoints for 0.7.x updaters and launchers; a current install removes the
|
|
15
|
+
// launchers themselves.
|
|
16
|
+
export const RETIRED_ENTRY_KINDS = Object.freeze(["acc-bootstrap", "acc-claude-channel"]);
|
|
28
17
|
|
|
29
18
|
// Bootstrap resolution deliberately needs only Node built-ins: no selected
|
|
30
19
|
// generation is imported until admission and its actual PID lease are durable.
|
|
@@ -43,11 +32,9 @@ export function invokedDirectly(url) {
|
|
|
43
32
|
|
|
44
33
|
function unavailable(kind) {
|
|
45
34
|
// Never reflect control file content or payloads into a client diagnostic.
|
|
46
|
-
if (kind === "acc-hook" || kind === "acc-
|
|
35
|
+
if (kind === "acc-hook" || kind === "acc-antigravity-relay") {
|
|
47
36
|
process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
|
|
48
37
|
process.exitCode = 0;
|
|
49
|
-
} else if (kind === "acc-bootstrap") {
|
|
50
|
-
process.exitCode = 1; // The vendor shell shim launches its original command.
|
|
51
38
|
} else {
|
|
52
39
|
process.stderr.write("acc: runtime unavailable; retry after the update or run acc update to recover\n");
|
|
53
40
|
process.exitCode = 4;
|
|
@@ -83,28 +70,12 @@ async function updateImplementation(packageRoot, control) {
|
|
|
83
70
|
export async function runEntry({ kind, packageRoot, managerRoot, managedRequired = false }) {
|
|
84
71
|
if (!ENTRY_KINDS.includes(kind)) throw new Error("unknown ACC entry point");
|
|
85
72
|
const command = kind === "acc" ? commandPrefix(process.argv.slice(2)).command : null;
|
|
86
|
-
const bootstrapOptions = kind === "acc-bootstrap" ? parseBootstrapOptions(process.argv.slice(2)) : null;
|
|
87
|
-
if (kind === "acc-bootstrap" && bootstrapOptions === null) {
|
|
88
|
-
if (process.env.ACC_BOOTSTRAP_DEBUG === "1") {
|
|
89
|
-
process.stderr.write("acc-bootstrap: usage: --adapter <id> --real-executable <path> --data-home <path>\n");
|
|
90
|
-
}
|
|
91
|
-
process.exitCode = 2;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
73
|
let selected = packageRoot;
|
|
95
74
|
let managed = null;
|
|
96
75
|
let managementOnly = false;
|
|
97
76
|
let root;
|
|
98
77
|
try {
|
|
99
|
-
|
|
100
|
-
const requestedRoot = requestedDataHome === null ? null
|
|
101
|
-
: await canonicalManagerRoot(path.join(requestedDataHome, "acc", "runtime"));
|
|
102
|
-
if (managerRoot && requestedDataHome !== null
|
|
103
|
-
&& requestedDataHome !== await canonicalManagerRoot(path.dirname(path.dirname(managerRoot)))) {
|
|
104
|
-
throw new Error("bootstrap cache home differs from its launcher");
|
|
105
|
-
}
|
|
106
|
-
root = await canonicalManagerRoot(managerRoot ?? requestedRoot ?? managerLocation());
|
|
107
|
-
if (requestedRoot !== null && root !== requestedRoot) throw new Error("bootstrap data home differs from its launcher");
|
|
78
|
+
root = await canonicalManagerRoot(managerRoot ?? managerLocation());
|
|
108
79
|
const control = await readControl(root);
|
|
109
80
|
if (control !== null) {
|
|
110
81
|
selected = control.active.root;
|
|
@@ -138,13 +109,6 @@ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired
|
|
|
138
109
|
} catch { /* Fall through to the same graceful unavailability below. */ }
|
|
139
110
|
}
|
|
140
111
|
unavailable(kind);
|
|
141
|
-
if (kind === "acc-claude-channel" && selected) {
|
|
142
|
-
try {
|
|
143
|
-
const { startInertChannel } = await import(pathToFileURL(path.join(selected,
|
|
144
|
-
"bin", "entrypoints", "claude-channel-stdio.mjs")).href);
|
|
145
|
-
startInertChannel();
|
|
146
|
-
} catch { /* The stderr notice above already fired either way. */ }
|
|
147
|
-
}
|
|
148
112
|
return;
|
|
149
113
|
}
|
|
150
114
|
const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", `${kind}.mjs`)).href);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { lstat, mkdtemp, open, readFile, rename, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { ENTRY_KINDS } from "./entry.mjs";
|
|
4
|
+
import { ENTRY_KINDS, RETIRED_ENTRY_KINDS } from "./entry.mjs";
|
|
5
5
|
import { managedDirectory, syncDirectory } from "./state.mjs";
|
|
6
6
|
|
|
7
7
|
const MODULES = ["entry.mjs", "command-prefix.mjs", "state.mjs", "generation-files.mjs",
|
|
@@ -14,8 +14,6 @@ async function durableFile(file, bytes, mode = 0o600) {
|
|
|
14
14
|
|
|
15
15
|
export const stablePaths = root => ({
|
|
16
16
|
cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
|
|
17
|
-
bootstrap: path.join(root, "bin", "acc-bootstrap.mjs"),
|
|
18
|
-
channel: path.join(root, "bin", "acc-claude-channel.mjs"),
|
|
19
17
|
antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
|
|
20
18
|
});
|
|
21
19
|
|
|
@@ -57,6 +55,10 @@ export async function writeLaunchers(root, packageRoot) {
|
|
|
57
55
|
await rename(temporary, path.join(bin, `${kind}.mjs`));
|
|
58
56
|
} finally { await rm(temporary, { force: true }); }
|
|
59
57
|
}
|
|
58
|
+
// A 0.7.x `claude` shim or plugin copy that still reaches for one of these
|
|
59
|
+
// finds nothing: the shim then launches the vendor command untouched, and a
|
|
60
|
+
// re-laid plugin names none of them.
|
|
61
|
+
for (const kind of RETIRED_ENTRY_KINDS) await rm(path.join(bin, `${kind}.mjs`), { force: true });
|
|
60
62
|
await syncDirectory(bin);
|
|
61
63
|
return stablePaths(root);
|
|
62
64
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { applyPlan, detectInstallation, livePolicyOf, loadOwnership, planInstallation
|
|
2
|
+
import { applyPlan, detectInstallation, livePolicyOf, loadOwnership, planInstallation }
|
|
3
3
|
from "@agents-can-communicate/installer";
|
|
4
4
|
import { ALL_ADAPTERS, clientContext, probeTimeout } from "../install-command.mjs";
|
|
5
5
|
import { stablePaths, writeLaunchers } from "./launchers.mjs";
|
|
@@ -17,7 +17,7 @@ export async function prepareRefresh({ control, root, env = process.env, callerP
|
|
|
17
17
|
// session open across the update keeps running from the outgoing copy until it
|
|
18
18
|
// restarts. Naming it here keeps that copy; everything older than it goes.
|
|
19
19
|
const context = { ...clientContext(control.home, path.join(dataHome, "acc"),
|
|
20
|
-
{ env,
|
|
20
|
+
{ env, dataHome }), ...stablePaths(root),
|
|
21
21
|
keepPreviousVersion: control.active.version };
|
|
22
22
|
const recorded = (await loadOwnership({ dataHome })).installs;
|
|
23
23
|
const detected = await detectInstallation({ adapters, context, probeTimeoutMs: probeTimeout(env) });
|
|
@@ -38,8 +38,7 @@ export async function prepareRefresh({ control, root, env = process.env, callerP
|
|
|
38
38
|
}
|
|
39
39
|
return async () => {
|
|
40
40
|
await writeLaunchers(root, control.pending.root);
|
|
41
|
-
const result = await applyPlan({ plan, adapters, context, dataHome, accVersion: control.pending.version
|
|
42
|
-
activation: { bootstrap: context.bootstrap } });
|
|
41
|
+
const result = await applyPlan({ plan, adapters, context, dataHome, accVersion: control.pending.version });
|
|
43
42
|
return { ...result, failed: result.failed.map(failure => ({ ...failure,
|
|
44
43
|
paths: plan.operations.find(operation => operation.adapterId === failure.adapterId)
|
|
45
44
|
?.artifacts?.map(artifact => artifact.path).filter(file => typeof file === "string") ?? [],
|
|
@@ -34,11 +34,11 @@ export function nativeRemediation(entry) {
|
|
|
34
34
|
+ " # opt in to automatic peer requests; may spend tokens");
|
|
35
35
|
} else if (native.configured && native.activation === "missing") {
|
|
36
36
|
steps.push(`acc install --adapter ${entry.adapterId}`
|
|
37
|
-
+ " # complete the missing native
|
|
37
|
+
+ " # complete the missing native delivery setup");
|
|
38
38
|
} else if (native.configured && native.runtime !== "active"
|
|
39
39
|
&& !["needed", "blocked", "unsupported"].includes(service?.state)) {
|
|
40
40
|
steps.push(`${entry.displayName}: no verified live channel in this workspace; `
|
|
41
|
-
+ "
|
|
41
|
+
+ "start a new client session and check its integration prompts; "
|
|
42
42
|
+ "then run acc doctor here");
|
|
43
43
|
}
|
|
44
44
|
if (native.reasonCode === "native_endpoint_unavailable") {
|
|
@@ -33,6 +33,9 @@ const ageBand = (session, age) =>
|
|
|
33
33
|
* @returns {"online" | "stale" | "offline"}
|
|
34
34
|
* @throws {AccError} EXIT.USAGE when pidIsAlive is not a function.
|
|
35
35
|
*/
|
|
36
|
+
// A client's end reason is a short word of its own ("clear", "logout").
|
|
37
|
+
const END_REASON = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
38
|
+
|
|
36
39
|
export function classifySessionPresence(session, now, pidIsAlive) {
|
|
37
40
|
// Required rather than defaulted. A probe that defaults to "everyone is
|
|
38
41
|
// alive" turns a forgotten call site into a check that silently passes, which
|
|
@@ -149,10 +152,14 @@ export function createSessionService(ports) {
|
|
|
149
152
|
}, { kinds: ["session"] });
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
async function closeSession({ sessionId, workspaceId, generation }) {
|
|
155
|
+
async function closeSession({ sessionId, workspaceId, generation, endReason = null }) {
|
|
156
|
+
// The client's own word for why, kept in extensions: an older ACC reading
|
|
157
|
+
// the same store skips them, and the session.closed event is unchanged.
|
|
158
|
+
const reason = typeof endReason === "string" && END_REASON.test(endReason) ? endReason : null;
|
|
153
159
|
const close = current => {
|
|
154
160
|
assertGeneration({ record: current }, generation, "close", workspaceId ?? store.workspaceId);
|
|
155
|
-
return { ...current, state: "closed", heartbeatAt: clock.now()
|
|
161
|
+
return { ...current, state: "closed", heartbeatAt: clock.now(),
|
|
162
|
+
...(reason === null ? {} : { extensions: { ...(current.extensions ?? {}), endReason: reason } }) };
|
|
156
163
|
};
|
|
157
164
|
let ephemeral = null;
|
|
158
165
|
await store.ephemeral.delete("session", sessionId, async current => {
|
|
@@ -179,6 +186,21 @@ export function createSessionService(ports) {
|
|
|
179
186
|
}, { kinds: ["session"] });
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
/** The participant's most recent session, open or closed, or null. */
|
|
190
|
+
async function lastSessionOf({ participantId, workspaceId }) {
|
|
191
|
+
const resolved = workspaceId ?? store.workspaceId;
|
|
192
|
+
const snapshot = resolved === undefined ? null
|
|
193
|
+
: await store.snapshot(resolved, { kinds: ["workspace", "session"] });
|
|
194
|
+
const records = snapshot?.workspace === null
|
|
195
|
+
? await store.ephemeral.list("session")
|
|
196
|
+
: snapshot?.sessions ?? await store.ephemeral.list("session");
|
|
197
|
+
const [last] = records.filter(session => session.participantId === participantId)
|
|
198
|
+
.sort((left, right) => Date.parse(right.heartbeatAt) - Date.parse(left.heartbeatAt)
|
|
199
|
+
|| right.sessionId.localeCompare(left.sessionId));
|
|
200
|
+
return last === undefined ? null : { sessionId: last.sessionId, generation: last.generation,
|
|
201
|
+
state: last.state, endReason: last.extensions?.endReason ?? null };
|
|
202
|
+
}
|
|
203
|
+
|
|
182
204
|
async function listLiveSessions({ participantId, workspaceId, now = clock.now() }) {
|
|
183
205
|
const resolved = workspaceId ?? store.workspaceId;
|
|
184
206
|
const snapshot = resolved === undefined ? null
|
|
@@ -198,6 +220,7 @@ export function createSessionService(ports) {
|
|
|
198
220
|
resumeSession,
|
|
199
221
|
heartbeatSession,
|
|
200
222
|
closeSession,
|
|
223
|
+
lastSessionOf,
|
|
201
224
|
listLiveSessions,
|
|
202
225
|
locateSession: locate,
|
|
203
226
|
ensureMaterialised: options => ensureMaterialised(ports, options),
|
|
@@ -105,6 +105,20 @@ export function createStatusService(ports, sessions, deliveryBindings) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// A participant with no live session but messages waiting for it stays in
|
|
109
|
+
// the roster, by its latest session, so a sender can see where they went.
|
|
110
|
+
const liveParticipants = new Set(live.map(item => item.session.participantId));
|
|
111
|
+
const latest = new Map();
|
|
112
|
+
for (const { session } of classified) {
|
|
113
|
+
const seen = latest.get(session.participantId);
|
|
114
|
+
if (seen === undefined || Date.parse(session.heartbeatAt) > Date.parse(seen.heartbeatAt)) {
|
|
115
|
+
latest.set(session.participantId, session);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const awaited = new Set([...latest.values()].filter(session => !liveParticipants.has(session.participantId)
|
|
119
|
+
&& (tally(session.participantId).queued + tally(session.participantId).offered) > 0)
|
|
120
|
+
.map(session => session.sessionId));
|
|
121
|
+
|
|
108
122
|
return {
|
|
109
123
|
workspaceId,
|
|
110
124
|
materialised: durable,
|
|
@@ -115,7 +129,8 @@ export function createStatusService(ports, sessions, deliveryBindings) {
|
|
|
115
129
|
// after a month of work this listed sixty entries for one live session.
|
|
116
130
|
// `acc status --all` is how the worktree-cleanup question is asked.
|
|
117
131
|
participants: classified
|
|
118
|
-
.filter(item => input.all === true || item.presence !== "offline"
|
|
132
|
+
.filter(item => input.all === true || item.presence !== "offline"
|
|
133
|
+
|| awaited.has(item.session.sessionId))
|
|
119
134
|
.map(({ session, presence }) => ({
|
|
120
135
|
sessionId: session.sessionId,
|
|
121
136
|
participantId: session.participantId,
|
|
@@ -132,6 +147,9 @@ export function createStatusService(ports, sessions, deliveryBindings) {
|
|
|
132
147
|
intent: intents.find(intent => intent.sessionId === session.sessionId
|
|
133
148
|
&& isCurrentIntent(intent))?.summary ?? null,
|
|
134
149
|
unretrieved: tally(session.participantId),
|
|
150
|
+
// The client's reason when it ended the session, such as Claude Code's
|
|
151
|
+
// "clear". Kept in the record's extensions, so it is null for 0.7.x.
|
|
152
|
+
...(presence === "offline" ? { endReason: session.extensions?.endReason ?? null } : {}),
|
|
135
153
|
})),
|
|
136
154
|
// The owner is named twice on purpose. Every command that reaches a peer
|
|
137
155
|
// takes a participant id, so a claim that gave only a session id sent the
|
|
@@ -5,7 +5,7 @@ import { refreshExpiredBinding } from "./refresh-binding.mjs";
|
|
|
5
5
|
const SAFE_ERRORS = new Set(["ambiguous_recipient_sessions", "delivery_disabled",
|
|
6
6
|
"recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected", "transport_permission_denied",
|
|
7
7
|
"unsupported_client_version"]);
|
|
8
|
-
const NAMED_LIVE_TRANSPORTS = new Set(["claude-
|
|
8
|
+
const NAMED_LIVE_TRANSPORTS = new Set(["claude-inbox", "codex-app-server"]);
|
|
9
9
|
|
|
10
10
|
const adaptersById = adapters => adapters instanceof Map
|
|
11
11
|
? adapters
|
|
@@ -72,10 +72,9 @@ export function createDeliveryRouter({ service, adapters, clock, platform = HOST
|
|
|
72
72
|
readLivePolicy }) {
|
|
73
73
|
const registry = adaptersById(adapters);
|
|
74
74
|
|
|
75
|
+
// Read at offer time, not from the binding: consent withdrawn after a session
|
|
76
|
+
// bound must stop the next offer, not the next rebind.
|
|
75
77
|
async function policyFor(adapter, binding) {
|
|
76
|
-
if (adapter?.nativeDelivery?.policySource !== "installation-record") {
|
|
77
|
-
return LIVE_POLICIES.has(binding.livePolicy) ? binding.livePolicy : "off";
|
|
78
|
-
}
|
|
79
78
|
if (typeof readLivePolicy !== "function") return "off";
|
|
80
79
|
try {
|
|
81
80
|
const policy = await readLivePolicy({ adapter, binding });
|
|
@@ -101,7 +100,14 @@ export function createDeliveryRouter({ service, adapters, clock, platform = HOST
|
|
|
101
100
|
// endpoint lease never sends a hook heartbeat: a daemon is reachability,
|
|
102
101
|
// not evidence that this particular session thread is still present.
|
|
103
102
|
const liveSessions = await service.listLiveSessions({ participantId, now });
|
|
104
|
-
if (liveSessions.length === 0)
|
|
103
|
+
if (liveSessions.length === 0) {
|
|
104
|
+
// No open session: the message waits until one starts or resumes. The
|
|
105
|
+
// client's own reason, when it gave one (Claude Code's /clear), is passed on.
|
|
106
|
+
const last = typeof service.lastSessionOf === "function"
|
|
107
|
+
? await service.lastSessionOf({ participantId }).catch(() => null) : null;
|
|
108
|
+
return { ...durable(participantId, "recipient_offline"),
|
|
109
|
+
...(last?.state === "closed" && typeof last.endReason === "string" ? { endReason: last.endReason } : {}) };
|
|
110
|
+
}
|
|
105
111
|
if (liveSessions.length > 1) {
|
|
106
112
|
return durable(participantId, "ambiguous_recipient_sessions");
|
|
107
113
|
}
|
|
@@ -110,7 +116,9 @@ export function createDeliveryRouter({ service, adapters, clock, platform = HOST
|
|
|
110
116
|
participantId, now, includeExpired: true }))
|
|
111
117
|
.filter(binding => binding.sessionId === target.sessionId
|
|
112
118
|
&& binding.generation === target.generation);
|
|
113
|
-
|
|
119
|
+
// Online, with nothing to push through: a CLI participant, or a client
|
|
120
|
+
// that bound no live transport. It reads its inbox, so it is not gone.
|
|
121
|
+
if (bindings.length === 0) return durable(participantId, "no_live_transport");
|
|
114
122
|
const evaluated = await Promise.all(bindings.map(async binding => {
|
|
115
123
|
const adapter = registry.get(binding.adapterId);
|
|
116
124
|
return { binding, adapter, policy: await policyFor(adapter, binding) };
|
|
@@ -118,7 +126,7 @@ export function createDeliveryRouter({ service, adapters, clock, platform = HOST
|
|
|
118
126
|
const permitted = evaluated.filter(({ policy }) => permits(policy, message.kind));
|
|
119
127
|
if (permitted.length === 0) return durable(participantId, "delivery_disabled");
|
|
120
128
|
const reachable = permitted.filter(({ binding }) => binding.availableModes.includes("livePush"));
|
|
121
|
-
if (reachable.length === 0) return durable(participantId, "
|
|
129
|
+
if (reachable.length === 0) return durable(participantId, "no_live_transport");
|
|
122
130
|
const capable = reachable.filter(({ binding, adapter }) => liveCapable(adapter, binding));
|
|
123
131
|
if (capable.length === 0) {
|
|
124
132
|
return durable(participantId, "unsupported_client_version");
|
|
@@ -214,6 +222,15 @@ export function createDeliveryRouter({ service, adapters, clock, platform = HOST
|
|
|
214
222
|
"unsupported_client_version");
|
|
215
223
|
return durable(participantId, "unsupported_client_version");
|
|
216
224
|
}
|
|
225
|
+
// A wake put a notice in front of the client, not the body. Recording it
|
|
226
|
+
// as offered would make the next-turn projection treat the body as already
|
|
227
|
+
// shown and leave it out of the very turn the wake started. The receipt
|
|
228
|
+
// stays queued; the hook records the offer once its stdout carried the body.
|
|
229
|
+
if (adapter.nativeDelivery?.offerKind === "wake") {
|
|
230
|
+
// The client took the wake but holds it for its user's approval.
|
|
231
|
+
return { recipientParticipantId: participantId, outcome: "woken", transport,
|
|
232
|
+
...(response.pendingApproval === true ? { pendingApproval: true } : {}) };
|
|
233
|
+
}
|
|
217
234
|
try {
|
|
218
235
|
await service.recordOfferSucceeded({ messageId: message.messageId,
|
|
219
236
|
recipientParticipantId: participantId, targetSessionId: binding.sessionId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { storeNativeAttempt } from "@agents-can-communicate/adapter-sdk";
|
|
2
2
|
import { readInstalledLivePolicyState } from "@agents-can-communicate/installer";
|
|
3
3
|
|
|
4
|
-
import { establishNativeBinding
|
|
4
|
+
import { establishNativeBinding } from "./native-binding.mjs";
|
|
5
5
|
|
|
6
6
|
// A cold worker plus the write has to finish inside this window. 250 ms was
|
|
7
7
|
// not enough on a busy CI runner, so the attempt file never appeared and
|
|
@@ -27,13 +27,11 @@ export async function bindNative({ adapter, event, hookBinding, clientVersion, p
|
|
|
27
27
|
if (Date.now() >= deadline) throw new Error("hook deadline expired");
|
|
28
28
|
};
|
|
29
29
|
assertBudget();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const { policy, policyStatus } =
|
|
34
|
-
|
|
35
|
-
: { policy: livePolicyFrom(context.env), policyStatus: raw === undefined ? "missing"
|
|
36
|
-
: !LIVE_POLICIES.includes(raw) ? "invalid" : raw === "off" ? "off" : "enabled" };
|
|
30
|
+
// The install record is the only consent a hook reads; see
|
|
31
|
+
// validateNativeDeliveryContract for the environment source this replaced.
|
|
32
|
+
const policySource = "installation-record";
|
|
33
|
+
const { policy, policyStatus } = await readInstalledLivePolicyState({ dataHome: context.dataHome,
|
|
34
|
+
adapterId: adapter.id });
|
|
37
35
|
assertBudget();
|
|
38
36
|
const result = await establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
39
37
|
livePolicy: policy, service: context.service, runtimeDir: paths.root,
|
|
@@ -20,15 +20,21 @@ const STATIC_REASONS = new Set(["native_delivery_unsupported", "platform_not_cap
|
|
|
20
20
|
"version_unavailable", "prerelease_not_captured", "below_minimum_version",
|
|
21
21
|
"known_bad_version"]);
|
|
22
22
|
|
|
23
|
-
// Only the value a successful owned shell bootstrap exported counts; anything
|
|
24
|
-
// else - missing, blank, differently cased, a number - is off.
|
|
25
|
-
export function livePolicyFrom(env) {
|
|
26
|
-
const value = env?.ACC_NATIVE_DELIVERY_POLICY;
|
|
27
|
-
return LIVE_POLICIES.includes(value) ? value : "off";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
23
|
const isPid = value => Number.isInteger(value) && value > 0;
|
|
31
24
|
|
|
25
|
+
// An adapter may write its private endpoint before the static rule below judges
|
|
26
|
+
// the handshake, or after this hook stopped waiting. Nothing publishes such an
|
|
27
|
+
// endpoint, so nothing would ever retire it, and a refused client binds again
|
|
28
|
+
// on every turn. Fail-open and never awaited past the adapter's own answer.
|
|
29
|
+
function discardEndpoint(adapter, handshake, runtimeDir) {
|
|
30
|
+
const opaqueEndpointRef = handshake?.opaqueEndpointRef;
|
|
31
|
+
if (typeof opaqueEndpointRef !== "string" || opaqueEndpointRef === ""
|
|
32
|
+
|| typeof adapter?.retireNativeSession !== "function") return Promise.resolve();
|
|
33
|
+
return Promise.resolve().then(() => adapter.retireNativeSession({
|
|
34
|
+
binding: { opaqueEndpointRef, clientVersion: handshake.clientVersion ?? null }, runtimeDir }))
|
|
35
|
+
.catch(() => {});
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export async function establishNativeBinding({ adapter, event, hookBinding, clientVersion,
|
|
33
39
|
platform, livePolicy, service, runtimeDir, clock, env,
|
|
34
40
|
timeoutMs = DEFAULT_TIMEOUT_MS, heartbeatCadenceMs = DEFAULT_CADENCE_MS }) {
|
|
@@ -57,16 +63,23 @@ export async function establishNativeBinding({ adapter, event, hookBinding, clie
|
|
|
57
63
|
let timer = null;
|
|
58
64
|
try {
|
|
59
65
|
const budget = Math.max(1, Math.floor(timeoutMs));
|
|
66
|
+
const pending = Promise.resolve().then(() => adapter.bindNativeSession({ event, clientPid,
|
|
67
|
+
clientVersion, runtimeDir, timeoutMs: budget, env }));
|
|
68
|
+
// An answer that lands after the budget has nobody left to use it.
|
|
69
|
+
let timedOut = false;
|
|
70
|
+
pending.then(late => { if (timedOut) discardEndpoint(adapter, late, runtimeDir); }, () => {});
|
|
60
71
|
const handshake = await Promise.race([
|
|
61
|
-
|
|
62
|
-
timeoutMs: budget, env }),
|
|
72
|
+
pending,
|
|
63
73
|
new Promise((_resolve, reject) => {
|
|
64
|
-
timer = setTimeout(() =>
|
|
65
|
-
|
|
74
|
+
timer = setTimeout(() => {
|
|
75
|
+
timedOut = true;
|
|
76
|
+
reject(Object.assign(new Error("native handshake timed out"), { code: "ETIMEDOUT" }));
|
|
77
|
+
}, budget);
|
|
66
78
|
}),
|
|
67
79
|
]);
|
|
68
80
|
const verdict = validateNativeHandshake(adapter, { clientVersion, platform, handshake });
|
|
69
81
|
if (!verdict.ok) {
|
|
82
|
+
await discardEndpoint(adapter, handshake, runtimeDir);
|
|
70
83
|
return outcome(STATIC_REASONS.has(verdict.reasonCode) ? "unsupported" : "degraded",
|
|
71
84
|
verdict.reasonCode);
|
|
72
85
|
}
|
|
@@ -506,7 +506,7 @@ const HANDLERS = {
|
|
|
506
506
|
const owner = { sessionId: binding.accSessionId, generation: binding.generation };
|
|
507
507
|
const current = await context.service.locateSession(binding.accSessionId);
|
|
508
508
|
if (current?.record.generation === binding.generation && current.record.state === "open") {
|
|
509
|
-
await context.service.closeSession(owner);
|
|
509
|
+
await context.service.closeSession({ ...owner, endReason: event.endReason ?? null });
|
|
510
510
|
} else {
|
|
511
511
|
// Close may have committed before this hook died. Retire only the old
|
|
512
512
|
// endpoint and binding; do not close a replacement or repeat a close event.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
1
4
|
import { applyServiceSetup } from "./service-setup.mjs";
|
|
2
5
|
import { applyNativeActivation, deactivateNative } from "./native-activation.mjs";
|
|
3
6
|
import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
@@ -52,12 +55,10 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
52
55
|
retainedMechanisms = report.retainedMechanisms;
|
|
53
56
|
}
|
|
54
57
|
let native = operation.retainedNativeActivation ?? null;
|
|
55
|
-
let appendedRcBlock = false;
|
|
56
58
|
if (operation.nativeActivation !== undefined) {
|
|
57
|
-
const applied = await applyNativeActivation({
|
|
58
|
-
activation: operation.nativeActivation,
|
|
59
|
+
const applied = await applyNativeActivation({
|
|
60
|
+
activation: operation.nativeActivation, ...activation });
|
|
59
61
|
native = applied.nativeActivation;
|
|
60
|
-
appendedRcBlock = applied.appendedRcBlock;
|
|
61
62
|
}
|
|
62
63
|
if (retainedMechanisms.length > 0) {
|
|
63
64
|
native = { ...(native ?? operation.deactivation),
|
|
@@ -71,7 +72,7 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
71
72
|
artifacts: operation.artifacts, createdDirectories,
|
|
72
73
|
deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
|
|
73
74
|
nativeActivation: native });
|
|
74
|
-
results.operations.push({ ...operation, applied: true,
|
|
75
|
+
results.operations.push({ ...operation, applied: true,
|
|
75
76
|
needsAction: outcome.needsAction ?? [],
|
|
76
77
|
changes: outcome.changes ?? [], diagnostics: [
|
|
77
78
|
...(operation.deliveryDiagnostic === undefined
|
|
@@ -118,6 +119,19 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
118
119
|
results.failed.push({ adapterId: operation.adapterId, error: error.message });
|
|
119
120
|
}
|
|
120
121
|
}
|
|
122
|
+
// The eligibility cache a 0.7.x `claude` shim read before every launch. No
|
|
123
|
+
// shim reads it any more, and it holds nothing a user wrote.
|
|
124
|
+
if (!dryRun && typeof dataHome === "string") {
|
|
125
|
+
await rm(path.join(dataHome, "acc", "native-bootstrap"), { recursive: true, force: true })
|
|
126
|
+
.catch(() => {});
|
|
127
|
+
// A 0.7.x Claude Channel unlinked its own registration on a clean exit;
|
|
128
|
+
// one that crashed left it in its workspace, naming a dead socket.
|
|
129
|
+
const workspaces = path.join(dataHome, "acc", "workspaces");
|
|
130
|
+
for (const workspace of await readdir(workspaces).catch(() => [])) {
|
|
131
|
+
await rm(path.join(workspaces, workspace, "native", "claude"), { recursive: true, force: true })
|
|
132
|
+
.catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
121
135
|
return results;
|
|
122
136
|
}
|
|
123
137
|
|
|
@@ -5,7 +5,7 @@ import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility,
|
|
|
5
5
|
validateNativeActivationPlan }
|
|
6
6
|
from "@agents-can-communicate/adapter-sdk";
|
|
7
7
|
|
|
8
|
-
import { resolveExecutable,
|
|
8
|
+
import { resolveExecutable, shimDirFor } from "./native-activation.mjs";
|
|
9
9
|
|
|
10
10
|
import { describeDeliveryFallback, describeNativeReason } from "./delivery-diagnostics.mjs";
|
|
11
11
|
|
|
@@ -83,8 +83,7 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
|
|
|
83
83
|
if (facts.eligibility.eligible !== true) {
|
|
84
84
|
const reasonCode = facts.eligibility.reasonCode ?? "feature_probe_failed";
|
|
85
85
|
// A stored policy can be consented to before a service/session exists.
|
|
86
|
-
// No activation is planned until the full probe passes
|
|
87
|
-
// clients still need their verified plan to disclose launch changes.
|
|
86
|
+
// No activation is planned until the full probe passes.
|
|
88
87
|
const consentAvailable = adapter.nativeDelivery.policySource === "installation-record"
|
|
89
88
|
&& ["native_endpoint_unavailable", "native_session_unavailable"].includes(reasonCode);
|
|
90
89
|
return STATIC_REASONS.has(reasonCode)
|
|
@@ -100,10 +99,6 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
|
|
|
100
99
|
return degraded("feature_probe_failed", facts);
|
|
101
100
|
}
|
|
102
101
|
if (!activationPlan.eligible) return degraded(activationPlan.reasonCode, facts);
|
|
103
|
-
const shell = context?.shell ?? null;
|
|
104
|
-
if (activationPlan.mechanisms.some(item => item.kind === "shell-bootstrap") && shell !== "zsh") {
|
|
105
|
-
return { ...degraded("unsupported_shell", facts), activationPlan };
|
|
106
|
-
}
|
|
107
102
|
return { state: "eligible", reasonCode: null, ...facts, activationPlan };
|
|
108
103
|
} catch {
|
|
109
104
|
return degraded("feature_probe_failed", { realExecutable: null, probe: null, eligibility: null });
|
|
@@ -165,6 +160,7 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
165
160
|
// on the machine down with it.
|
|
166
161
|
if (detected.blocked) entry.blocked = detected.blocked;
|
|
167
162
|
if (detected.outgoingDelivery) entry.outgoingDelivery = detected.outgoingDelivery;
|
|
163
|
+
if (detected.inboundDelivery) entry.inboundDelivery = detected.inboundDelivery;
|
|
168
164
|
if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
|
|
169
165
|
entry.diagnostics = [...(detected.diagnostics ?? [])];
|
|
170
166
|
// What a person has to do, as opposed to what is true. Adapters that
|
|
@@ -5,13 +5,10 @@ export { applyPlan } from "./apply.mjs";
|
|
|
5
5
|
export { finalizeRemoval, fingerprint, loadOwnership, missingArtifactParents, recordInstall,
|
|
6
6
|
removeEmptyOwnedDirectories, removeOwned, removeOwnedArtifacts, treeFingerprint, verifyOwned }
|
|
7
7
|
from "./ownership.mjs";
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
shellLiteral, uninstallShellBootstrap, validateShimEntry } from "./shell-bootstrap.mjs";
|
|
13
|
-
export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf, rcFileFor,
|
|
14
|
-
resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|
|
8
|
+
export { BLOCK_BEGIN, BLOCK_END, SHIM_MARKER, locateBlock, uninstallShellBootstrap }
|
|
9
|
+
from "./shell-bootstrap.mjs";
|
|
10
|
+
export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf,
|
|
11
|
+
resolveExecutable, shimDirFor } from "./native-activation.mjs";
|
|
15
12
|
export { readInstalledLivePolicy, readInstalledLivePolicyState } from "./live-policy.mjs";
|
|
16
13
|
export { describeDeliveryFallback, describeInstallDelivery, describeNativeReason }
|
|
17
14
|
from "./delivery-diagnostics.mjs";
|