agents-can-communicate 0.10.4 → 0.11.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.
- package/README.md +29 -21
- package/bin/acc-indicator.mjs +5 -0
- package/bin/entrypoints/acc-antigravity-relay.mjs +2 -2
- package/bin/entrypoints/acc-hook.mjs +4 -24
- package/bin/entrypoints/acc-mcp.mjs +8 -6
- package/bin/entrypoints/acc.mjs +22 -11
- package/bin/entrypoints/antigravity-relay-binding.mjs +1 -1
- package/docs/CAPABILITIES.md +3 -0
- package/docs/CLI.md +34 -7
- package/docs/MCP.md +42 -2
- package/docs/PROTOCOL.md +19 -0
- package/docs/STATUS_INDICATOR.md +133 -0
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WHY_ACC.md +14 -8
- 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-antigravity/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +83 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +29 -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/extension/skills/acc/SKILL.md +29 -5
- 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-grok/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator.mjs +23 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +9 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator.mjs +17 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +12 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +5 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-command.mjs +14 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-text.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt-writer.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-order.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +40 -27
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
- package/node_modules/@agents-can-communicate/cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/cli/src/adapter-catalog.mjs +33 -0
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +17 -11
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +38 -110
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +19 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/recorded-text.mjs +32 -0
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
- package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +5 -2
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +2 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -7
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +23 -11
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +66 -26
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +3 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/state-reads.mjs +24 -7
- package/package.json +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AccError, EXIT, SCHEMA_VERSION,
|
|
2
|
-
|
|
1
|
+
import { AccError, EXIT, SCHEMA_VERSION, createId, validateRecord }
|
|
2
|
+
from "@agents-can-communicate/protocol";
|
|
3
3
|
|
|
4
|
+
import { claimResources, releaseSelectors, selectReleaseClaims } from "./claim-selectors.mjs";
|
|
4
5
|
import { ensureMaterialised } from "./materialisation.mjs";
|
|
5
6
|
import { classifySessionPresence } from "./sessions.mjs";
|
|
6
7
|
|
|
@@ -33,10 +34,11 @@ const isLive = (claim, now) => Date.parse(claim.expiresAt) > Date.parse(now);
|
|
|
33
34
|
export function createClaimService(ports, sessions) {
|
|
34
35
|
const { store, clock, ids, pidIsAlive } = ports;
|
|
35
36
|
|
|
36
|
-
async function requireOwner(input, action, tx) {
|
|
37
|
+
async function requireOwner(input, action, tx, allowEphemeral = false) {
|
|
37
38
|
const current = tx === undefined
|
|
38
39
|
? (await sessions.locateSession(input.sessionId, input.workspaceId))?.record
|
|
39
|
-
: tx.get("session", input.sessionId)
|
|
40
|
+
: tx.get("session", input.sessionId)
|
|
41
|
+
?? (allowEphemeral ? await store.ephemeral.get("session", input.sessionId) : null);
|
|
40
42
|
if (current == null || current.state !== "open") {
|
|
41
43
|
throw new AccError(EXIT.CONFLICT, `cannot ${action} from a session that is not open`,
|
|
42
44
|
{ sessionId: input.sessionId });
|
|
@@ -65,63 +67,66 @@ export function createClaimService(ports, sessions) {
|
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
async function
|
|
70
|
+
async function acquire(input, resources, batch) {
|
|
69
71
|
const session = await requireOwner(input, "claim");
|
|
70
|
-
|
|
71
|
-
// claim taken over MCP and one taken from the CLI mean the same thing - and
|
|
72
|
-
// a shape that could never match is refused rather than stored.
|
|
73
|
-
assertMatchableResource(input.resource, message => {
|
|
74
|
-
throw new AccError(EXIT.USAGE, message, { resource: input.resource });
|
|
75
|
-
});
|
|
76
|
-
const resource = normaliseResource(input.resource);
|
|
72
|
+
resources = claimResources(resources);
|
|
77
73
|
const workspaceId = session.workspaceId;
|
|
78
74
|
await ensureMaterialised(ports, { workspaceId, descriptor: input.descriptor,
|
|
79
75
|
reason: "durable_object" });
|
|
80
|
-
|
|
81
|
-
const expiresAt = new Date(Date.parse(now) + (input.leaseSeconds ?? 1800) * 1000)
|
|
82
|
-
.toISOString();
|
|
83
|
-
// Only to name the owner of a conflicting claim in the error.
|
|
84
|
-
const snapshot = await store.snapshot(workspaceId, { kinds: ["session"] });
|
|
85
|
-
|
|
86
|
-
let record = null;
|
|
87
|
-
await store.transaction(async tx => {
|
|
76
|
+
return store.transaction(async tx => {
|
|
88
77
|
await requireOwner(input, "claim", tx);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
78
|
+
const now = clock.now();
|
|
79
|
+
const expiresAt = new Date(Date.parse(now) + (input.leaseSeconds ?? 1800) * 1000)
|
|
80
|
+
.toISOString();
|
|
81
|
+
const existing = tx.list("claim");
|
|
82
|
+
const live = existing.filter(claim => isLive(claim, now));
|
|
83
|
+
const mode = input.mode ?? "exclusive";
|
|
84
|
+
const conflicts = resources.flatMap(resource => live
|
|
85
|
+
.filter(claim => overlaps(claim.resource, resource)
|
|
86
|
+
&& claim.ownerSessionId !== session.sessionId
|
|
87
|
+
&& (claim.mode === "exclusive" || mode === "exclusive"))
|
|
88
|
+
.map(claim => ({ requestedResource: resource,
|
|
89
|
+
...conflictWith(claim, session, now, tx.list("session")).details })));
|
|
90
|
+
if (conflicts.length > 0) {
|
|
91
|
+
if (!batch) {
|
|
92
|
+
const { requestedResource, ...details } = conflicts[0];
|
|
93
|
+
throw new AccError(EXIT.CONFLICT, "the resource is already claimed", details);
|
|
94
|
+
}
|
|
95
|
+
const message = "batch claim conflicts:\n" + conflicts.map(conflict =>
|
|
96
|
+
`${conflict.requestedResource} overlaps ${conflict.resource}, held by ${conflict.ownerSessionId}`)
|
|
97
|
+
.join("\n");
|
|
98
|
+
throw new AccError(EXIT.CONFLICT, message, { conflicts });
|
|
99
|
+
}
|
|
100
|
+
// Validate every record before staging a write, including renewals.
|
|
101
|
+
const records = resources.map(resource => {
|
|
102
|
+
const mine = existing.find(claim => claim.ownerSessionId === session.sessionId
|
|
103
|
+
&& claim.resource === resource);
|
|
104
|
+
const claimId = mine?.claimId ?? createId("claim");
|
|
105
|
+
return { mine, record: validateRecord("claim", {
|
|
106
|
+
schemaVersion: SCHEMA_VERSION,
|
|
107
|
+
claimId,
|
|
108
|
+
workspaceId,
|
|
109
|
+
ownerSessionId: session.sessionId,
|
|
110
|
+
resource,
|
|
111
|
+
mode,
|
|
112
|
+
enforcement: input.enforcement ?? "advisory",
|
|
113
|
+
reason: input.reason,
|
|
114
|
+
// Reuse a lapsed record, but start a new period of holding it.
|
|
115
|
+
acquiredAt: mine !== undefined && isLive(mine, now) ? mine.acquiredAt : now,
|
|
116
|
+
expiresAt,
|
|
117
|
+
generation: ids.next("generation"),
|
|
118
|
+
}) };
|
|
117
119
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
for (const { mine, record } of records) {
|
|
121
|
+
const { claimId, resource } = record;
|
|
122
|
+
tx.put("claim", claimId, record, tx.generationOf("claim", claimId));
|
|
123
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"), workspaceId,
|
|
124
|
+
actorSessionId: session.sessionId, type: mine === undefined ? "claim.acquired"
|
|
125
|
+
: "claim.renewed", occurredAt: now,
|
|
126
|
+
payload: { claimId, resource, mode: record.mode } });
|
|
127
|
+
}
|
|
128
|
+
return records.map(item => item.record);
|
|
123
129
|
}, { kinds: ["session", "claim"] });
|
|
124
|
-
return record;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
async function renewClaim(input) {
|
|
@@ -146,40 +151,42 @@ export function createClaimService(ports, sessions) {
|
|
|
146
151
|
return record;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
async function
|
|
154
|
+
async function removeClaims(input, selectors, { authority = null, reason = null } = {}) {
|
|
150
155
|
const session = await requireOwner(input, "release");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
return store.transaction(async tx => {
|
|
157
|
+
await requireOwner(input, "release", tx, true);
|
|
158
|
+
const now = clock.now();
|
|
159
|
+
const selected = selectReleaseClaims(tx, selectors, session.sessionId);
|
|
160
|
+
for (const existing of selected) {
|
|
161
|
+
const owned = existing.ownerSessionId === session.sessionId;
|
|
162
|
+
if (!owned && authority !== "human" && authority !== "policy") {
|
|
163
|
+
throw new AccError(EXIT.CONFLICT,
|
|
164
|
+
"force release requires human or policy authority",
|
|
165
|
+
{ claimId: existing.claimId, authority });
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
for (const existing of selected) {
|
|
169
|
+
const owned = existing.ownerSessionId === session.sessionId;
|
|
170
|
+
// Remove the live record; per-resource events retain its history.
|
|
171
|
+
tx.remove("claim", existing.claimId, tx.generationOf("claim", existing.claimId));
|
|
172
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
173
|
+
workspaceId: existing.workspaceId, actorSessionId: session.sessionId,
|
|
174
|
+
type: owned ? "claim.released" : "claim.force_released", occurredAt: now,
|
|
175
|
+
payload: { claimId: existing.claimId, authority,
|
|
176
|
+
reason, replacedGeneration: existing.generation } });
|
|
165
177
|
}
|
|
166
|
-
|
|
167
|
-
// not a claim, and every consumer of a snapshot would otherwise have to
|
|
168
|
-
// re-derive liveness. The event log keeps the history.
|
|
169
|
-
tx.remove("claim", input.claimId, tx.generationOf("claim", input.claimId));
|
|
170
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
171
|
-
workspaceId: existing.workspaceId, actorSessionId: session.sessionId,
|
|
172
|
-
type: owned ? "claim.released" : "claim.force_released", occurredAt: now,
|
|
173
|
-
payload: { claimId: input.claimId, authority: authority ?? null,
|
|
174
|
-
reason: reason ?? null, replacedGeneration: existing.generation } });
|
|
178
|
+
return selected;
|
|
175
179
|
}, { kinds: ["session", "claim"] });
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
return {
|
|
179
|
-
acquireClaim,
|
|
183
|
+
acquireClaim: async input => (await acquire(input, [input.resource], false))[0],
|
|
184
|
+
acquireClaims: input => acquire(input, input.resources, true),
|
|
180
185
|
renewClaim,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
releaseClaims: async input => removeClaims(input, releaseSelectors(input)),
|
|
187
|
+
releaseClaim: async input => { await removeClaims(input, { claimIds: [input.claimId] }); },
|
|
188
|
+
forceReleaseClaim: async input => { await removeClaims(input, { claimIds: [input.claimId] },
|
|
189
|
+
{ authority: input.authority, reason: input.reason });
|
|
190
|
+
},
|
|
184
191
|
};
|
|
185
192
|
}
|
|
@@ -6,7 +6,8 @@ export async function nextTurnDelivery(store, input = {}) {
|
|
|
6
6
|
return { queuedMessages: [], liveOfferedMessageIds: [], reminderMessageIds: [],
|
|
7
7
|
roomMessageIds: [], repeatMessages: [], repeatOffers: [] };
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
// A caller that has just read the store hands that read over (#298).
|
|
10
|
+
const snapshot = input.snapshot ?? await store.snapshot(input.workspaceId ?? store.workspaceId,
|
|
10
11
|
{ kinds: ["message", "receipt"] });
|
|
11
12
|
const receiptByMessage = new Map(snapshot.receipts
|
|
12
13
|
.filter(item => item.recipientParticipantId === input.participantId)
|
|
@@ -101,12 +101,15 @@ export function createSessionService(ports) {
|
|
|
101
101
|
: await store.ephemeral.update("session", sessionId, async current =>
|
|
102
102
|
current !== null && !await isMaterialised(store, current.workspaceId) ? beat(current) : null);
|
|
103
103
|
if (ephemeral !== null) return ephemeral;
|
|
104
|
-
|
|
104
|
+
// One session is read, not every session the workspace ever had: a turn's
|
|
105
|
+
// heartbeat took about a second in a workspace with 2,234 of them.
|
|
106
|
+
return store.transaction(async tx => {
|
|
107
|
+
await tx.load("session", sessionId);
|
|
105
108
|
const beaten = beat(tx.get("session", sessionId));
|
|
106
109
|
if (beaten === null) throw new AccError(EXIT.CONFLICT, "session is not open", { sessionId });
|
|
107
110
|
tx.put("session", sessionId, beaten, tx.generationOf("session", sessionId));
|
|
108
111
|
return beaten;
|
|
109
|
-
}, { kinds: ["session"] });
|
|
112
|
+
}, { kinds: ["session"], exactKinds: ["session"] });
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
/**
|
|
@@ -71,7 +71,8 @@ export function createSyncService(ports, sessions) {
|
|
|
71
71
|
const located = input.sessionId === undefined
|
|
72
72
|
? null : await sessions.locateSession(input.sessionId, workspaceId);
|
|
73
73
|
const session = located?.record ?? null;
|
|
74
|
-
|
|
74
|
+
// A caller that reads the store once for a whole turn hands that read over (#298).
|
|
75
|
+
const durable = input.snapshot ?? await store.snapshot(workspaceId);
|
|
75
76
|
const snapshot = durable.workspace !== null
|
|
76
77
|
? durable
|
|
77
78
|
: { ...durable,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { NATIVE_REASON_CODES, evaluateVersionContract, isLaunchOption }
|
|
1
|
+
import { NATIVE_REASON_CODES, evaluateVersionContract, isLaunchOption, OFFER_ERRORS, storeOfferObservation }
|
|
2
2
|
from "@agents-can-communicate/adapter-sdk";
|
|
3
3
|
|
|
4
4
|
import { lastNativeReason } from "./native-reason.mjs";
|
|
5
5
|
import { refreshExpiredBinding } from "./refresh-binding.mjs";
|
|
6
6
|
|
|
7
|
-
const SAFE_ERRORS = new Set(
|
|
8
|
-
"recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected", "transport_permission_denied",
|
|
9
|
-
"unsupported_client_version"]);
|
|
7
|
+
const SAFE_ERRORS = new Set(OFFER_ERRORS);
|
|
10
8
|
const NAMED_LIVE_TRANSPORTS = new Set(["claude-inbox", "codex-app-server"]);
|
|
11
9
|
|
|
12
10
|
const adaptersById = adapters => adapters instanceof Map
|
|
@@ -47,9 +45,20 @@ function safeTransport(value, opaqueEndpointRef) {
|
|
|
47
45
|
return opaqueEndpointRef === "live-adapter" ? "native-live" : "live-adapter";
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
48
|
+
export function createDeliveryRouter({ service, adapters, loadAdapter, clock, readLivePolicy,
|
|
51
49
|
readNativeReason = input => lastNativeReason({ runtimeDir: service.store?.root, ...input }) }) {
|
|
52
50
|
const registry = adaptersById(adapters);
|
|
51
|
+
// A command that routes one message loads only its recipients' adapters (#270).
|
|
52
|
+
const adapterFor = typeof loadAdapter === "function" ? loadAdapter
|
|
53
|
+
: async adapterId => registry.get(adapterId);
|
|
54
|
+
const observeOffer = (binding, reasonCode = null) => {
|
|
55
|
+
// Capture time and order now, but never await optional diagnostic I/O on
|
|
56
|
+
// the delivery path. The SDK bounds its own lifetime for short-lived CLIs.
|
|
57
|
+
void storeOfferObservation({
|
|
58
|
+
runtimeDir: service.store?.root, workspaceId: service.store?.workspaceId,
|
|
59
|
+
sessionId: binding.sessionId, generation: binding.generation, at: clock.now(),
|
|
60
|
+
state: reasonCode === null ? "active" : "degraded", reasonCode }).catch(() => {});
|
|
61
|
+
};
|
|
53
62
|
|
|
54
63
|
// A session that bound no live transport may have said why: the reason its
|
|
55
64
|
// own last native attempt recorded, from the closed vocabulary only, and the
|
|
@@ -82,6 +91,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
82
91
|
targetGeneration: binding.generation,
|
|
83
92
|
transport: safeTransport(transport, binding.opaqueEndpointRef), adapterId: binding.adapterId,
|
|
84
93
|
clientVersion: binding.clientVersion, safeErrorCode }).catch(() => null);
|
|
94
|
+
observeOffer(binding, safeErrorCode);
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
async function offerTo(message, participantId, now) {
|
|
@@ -112,7 +122,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
112
122
|
// that bound no live transport. It reads its inbox, so it is not gone.
|
|
113
123
|
if (bindings.length === 0) return withoutLiveTransport(participantId, target);
|
|
114
124
|
const evaluated = await Promise.all(bindings.map(async binding => {
|
|
115
|
-
const adapter =
|
|
125
|
+
const adapter = await adapterFor(binding.adapterId);
|
|
116
126
|
return { binding, adapter, policy: await policyFor(adapter, binding) };
|
|
117
127
|
}));
|
|
118
128
|
const permitted = evaluated.filter(({ policy }) => permits(policy, message.kind));
|
|
@@ -135,7 +145,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
135
145
|
if (!refreshed.refreshed) {
|
|
136
146
|
if (refreshed.errorCode === "transport_permission_denied") {
|
|
137
147
|
await recordFailure(binding, message, participantId, "live-adapter", refreshed.errorCode);
|
|
138
|
-
}
|
|
148
|
+
} else observeOffer(binding, refreshed.errorCode);
|
|
139
149
|
return durable(participantId, refreshed.errorCode);
|
|
140
150
|
}
|
|
141
151
|
const current = (await service.listDeliveryBindings({
|
|
@@ -216,6 +226,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
216
226
|
// shown and leave it out of the very turn the wake started. The receipt
|
|
217
227
|
// stays queued; the hook records the offer once its stdout carried the body.
|
|
218
228
|
if (adapter.nativeDelivery?.offerKind === "wake") {
|
|
229
|
+
observeOffer(binding, response.pendingApproval === true ? "inbound_approval_required" : null);
|
|
219
230
|
// The client took the wake but holds it for its user's approval.
|
|
220
231
|
return { recipientParticipantId: participantId, outcome: "woken", transport,
|
|
221
232
|
...(response.pendingApproval === true ? { pendingApproval: true } : {}) };
|
|
@@ -229,6 +240,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
229
240
|
await recordFailure(binding, message, participantId, transport, "transport_error");
|
|
230
241
|
return durable(participantId, "transport_error");
|
|
231
242
|
}
|
|
243
|
+
observeOffer(binding);
|
|
232
244
|
return { recipientParticipantId: participantId, outcome: "offered", transport };
|
|
233
245
|
}
|
|
234
246
|
|
|
@@ -102,19 +102,31 @@ export function resolveClient({ table, from, command, clientPackage, identify,
|
|
|
102
102
|
const entry = table.get(current);
|
|
103
103
|
if (entry === undefined || seen.has(current)) return null;
|
|
104
104
|
seen.add(current);
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
return { pid: current, entry, certificationName: product.certificationName,
|
|
108
|
-
version: product.version };
|
|
109
|
-
}
|
|
110
|
-
// `ps` reports some entries bare (`claude`) and some with a path
|
|
111
|
-
// (`/bin/zsh`), so the comparison has to be on the basename.
|
|
112
|
-
if (imageName(entry.comm) === command
|
|
113
|
-
|| scriptsOf(entry, clientPackage).some(name => name === command || name === CLIENT_PACKAGE)) {
|
|
114
|
-
return { pid: current, entry, certificationName: undefined, version: undefined };
|
|
115
|
-
}
|
|
105
|
+
const match = clientMatch(entry, { command, clientPackage, identify });
|
|
106
|
+
if (match !== null) return { pid: current, entry, ...match };
|
|
116
107
|
if (entry.ppid === current || entry.ppid <= 1) return null;
|
|
117
108
|
current = entry.ppid;
|
|
118
109
|
}
|
|
119
110
|
return null;
|
|
120
111
|
}
|
|
112
|
+
|
|
113
|
+
/** What one process-table entry is as the declared client, or null. */
|
|
114
|
+
function clientMatch(entry, { command, clientPackage, identify }) {
|
|
115
|
+
const product = recognised(identify, entry);
|
|
116
|
+
if (product !== null) {
|
|
117
|
+
return { certificationName: product.certificationName, version: product.version };
|
|
118
|
+
}
|
|
119
|
+
// `ps` reports some entries bare (`claude`) and some with a path
|
|
120
|
+
// (`/bin/zsh`), so the comparison has to be on the basename.
|
|
121
|
+
if (imageName(entry.comm) === command
|
|
122
|
+
|| scriptsOf(entry, clientPackage).some(name => name === command || name === CLIENT_PACKAGE)) {
|
|
123
|
+
return { certificationName: undefined, version: undefined };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Whether one entry is the client, as `resolveClient` would decide: a table
|
|
129
|
+
* read can stop there instead of reading the client's own parents. */
|
|
130
|
+
export function isClientProcess(entry, { command, clientPackage, identify }) {
|
|
131
|
+
return clientMatch(entry, { command, clientPackage, identify }) !== null;
|
|
132
|
+
}
|
|
@@ -15,7 +15,7 @@ import { clearPin, createGitProbe, resolveHookWorkspace, platformDataHome, runti
|
|
|
15
15
|
writePin as writeRuntimePin }
|
|
16
16
|
from "@agents-can-communicate/cli/hook-support";
|
|
17
17
|
|
|
18
|
-
import { resolveClient } from "./client-pid.mjs";
|
|
18
|
+
import { isClientProcess, resolveClient } from "./client-pid.mjs";
|
|
19
19
|
import { ownExecutable, probeClientVersion as defaultProbeClientVersion, probeExecutableVersion }
|
|
20
20
|
from "./client-version.mjs";
|
|
21
21
|
import { bindNative, callRelease, nativeActivationHintFor, nativeDiagnosticDeadline }
|
|
@@ -44,6 +44,17 @@ const WINDOWS = process.platform === "win32";
|
|
|
44
44
|
const PROCESS_TABLE_MS = WINDOWS ? 3_000 : 1_000;
|
|
45
45
|
const TABLE_RESERVE_MS = WINDOWS ? 500 : 0;
|
|
46
46
|
|
|
47
|
+
// How this adapter's client is recognised in a process table.
|
|
48
|
+
const clientOf = adapter => ({ command: adapter.client?.command ?? null,
|
|
49
|
+
clientPackage: adapter.client?.package, identify: adapter.identifyClientProcess });
|
|
50
|
+
|
|
51
|
+
// One bounded table read for a hook, which ends at the client it is looking for.
|
|
52
|
+
function readClientTable(readProcessTable, adapter, deadline) {
|
|
53
|
+
return Promise.resolve(readProcessTable({
|
|
54
|
+
timeoutMs: Math.max(1, Math.min(PROCESS_TABLE_MS, deadline - Date.now() - TABLE_RESERVE_MS)),
|
|
55
|
+
until: entry => isClientProcess(entry, clientOf(adapter)) }));
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function assertHookBudget(deadline, message = "hook deadline expired") {
|
|
48
59
|
if (Date.now() >= deadline) throw new Error(message);
|
|
49
60
|
}
|
|
@@ -252,8 +263,12 @@ async function openContext({ event, adapterId, dataHome, runtime, env, deadline
|
|
|
252
263
|
// the adapter's context envelope, at the end of one it is a continuation.
|
|
253
264
|
async function projectTurn({ binding, context, adapter, adapterId, render = null,
|
|
254
265
|
activationHint = null, activationNotice = null, repeats = true }) {
|
|
266
|
+
// One read of the store serves the whole projection. Sync and next-turn
|
|
267
|
+
// delivery each read every message and receipt the workspace held, twice a
|
|
268
|
+
// prompt, and nothing is written between the two (#298).
|
|
269
|
+
const snapshot = await context.service.store.snapshot(context.descriptor.id);
|
|
255
270
|
const sync = await context.service.sync({ sessionId: binding.accSessionId,
|
|
256
|
-
cursor: null, scope: "delta" });
|
|
271
|
+
cursor: null, scope: "delta", snapshot });
|
|
257
272
|
|
|
258
273
|
// Sync already carries the roster. Calling collectStatus here used to read
|
|
259
274
|
// the entire materialised store a second time on every prompt merely to
|
|
@@ -277,7 +292,7 @@ async function projectTurn({ binding, context, adapter, adapterId, render = null
|
|
|
277
292
|
workspaceId: context.descriptor.id,
|
|
278
293
|
participantId: mine?.participantId,
|
|
279
294
|
exceptSessionId: binding.accSessionId,
|
|
280
|
-
repeats: repeats && canOfferNextTurn });
|
|
295
|
+
repeats: repeats && canOfferNextTurn, snapshot });
|
|
281
296
|
// New bodies first, so a repeat is what the byte budget drops.
|
|
282
297
|
const messages = [...delivery.queuedMessages, ...delivery.repeatMessages];
|
|
283
298
|
const repeating = new Set(delivery.repeatMessages.map(message => message.messageId));
|
|
@@ -498,8 +513,8 @@ const HANDLERS = {
|
|
|
498
513
|
// each can start a program. A session start began the read already, when
|
|
499
514
|
// its event was known. Handled here so a probe that fails first leaves no
|
|
500
515
|
// rejection unobserved.
|
|
501
|
-
const tableRead = known || command === null ? null
|
|
502
|
-
|
|
516
|
+
const tableRead = known || command === null ? null
|
|
517
|
+
: started ?? readClientTable(readProcessTable, adapter, deadline);
|
|
503
518
|
tableRead?.catch(() => {});
|
|
504
519
|
// The binary on PATH answers in parallel, as the fallback: the version the
|
|
505
520
|
// running client names for itself, when it names one, wins below.
|
|
@@ -507,8 +522,7 @@ const HANDLERS = {
|
|
|
507
522
|
{ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
|
|
508
523
|
mark("reading the process table");
|
|
509
524
|
const client = known ? { pid: knownClientPid } : command === null ? null
|
|
510
|
-
: resolveClient({ table: await tableRead, from: process.pid,
|
|
511
|
-
clientPackage: adapter.client?.package, identify: adapter.identifyClientProcess });
|
|
525
|
+
: resolveClient({ table: await tableRead, from: process.pid, ...clientOf(adapter) });
|
|
512
526
|
assertHookBudget(deadline);
|
|
513
527
|
mark("probing the client version");
|
|
514
528
|
const product = declaredProduct(adapter, client);
|
|
@@ -535,9 +549,9 @@ const HANDLERS = {
|
|
|
535
549
|
};
|
|
536
550
|
if (event.kind === "beforeTurn" && binding !== null) {
|
|
537
551
|
// A genuine prompt can resume after an ephemeral removal or a durable
|
|
538
|
-
// close, or carry on a session whose client restarted
|
|
539
|
-
// probes: a CLI replacement can run outside the
|
|
540
|
-
// Never adopt that replacement's identity.
|
|
552
|
+
// close, or carry on a session whose client restarted or was found only
|
|
553
|
+
// now. Recheck after probes: a CLI replacement can run outside the
|
|
554
|
+
// native lifecycle lock. Never adopt that replacement's identity.
|
|
541
555
|
// A session can also close between the turn's check and this one while
|
|
542
556
|
// its client was down; that is the closed-session resumption below,
|
|
543
557
|
// not a replacement.
|
|
@@ -665,14 +679,28 @@ const HANDLERS = {
|
|
|
665
679
|
const turn = await projectActivation({ ...input, binding: fresh }, offered);
|
|
666
680
|
return { ...turn, nativeBinding: started.nativeBinding };
|
|
667
681
|
}
|
|
668
|
-
|
|
682
|
+
// A start that could not find its client opened the session without it:
|
|
683
|
+
// on 2026-10-12, at load average 56-84, the table read outlasted its limit.
|
|
684
|
+
// Every turn looks again until the client is found; nothing else would,
|
|
685
|
+
// and the session would stay without live delivery for its whole life.
|
|
686
|
+
// Only the open generation this binding names is rebound; a stale binding
|
|
687
|
+
// never reaches the start path, whatever the table says.
|
|
688
|
+
const known = Number.isInteger(binding.clientPid) && binding.clientPid > 0;
|
|
689
|
+
const bindable = current.record.state === "open" && current.record.generation === binding.generation;
|
|
690
|
+
const found = known || !bindable || clientOf(adapter).command === null ? null
|
|
691
|
+
: await readClientTable(input.readProcessTable, adapter, deadline);
|
|
692
|
+
const lateClient = found !== null
|
|
693
|
+
&& resolveClient({ table: found, from: process.pid, ...clientOf(adapter) }) !== null;
|
|
694
|
+
if (lateClient || clientGone(binding)) {
|
|
669
695
|
// The client this session was bound to has exited, and a turn arrived
|
|
670
696
|
// anyway: the client restarted under the same conversation. Antigravity
|
|
671
697
|
// 2.0 reopens a conversation without a second SessionStart, so without
|
|
672
698
|
// this the session stayed bound to a dead process and lost live delivery
|
|
673
699
|
// for good. The start path finds the client running now, keeps this ACC
|
|
674
|
-
// session and generation, and binds again.
|
|
675
|
-
|
|
700
|
+
// session and generation, and binds again. A client found only now
|
|
701
|
+
// takes the same path, with the table this turn already read.
|
|
702
|
+
const started = await HANDLERS.sessionStart({ ...input, clientRestarted: true,
|
|
703
|
+
...(lateClient ? { tableRead: Promise.resolve(found) } : {}) });
|
|
676
704
|
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
677
705
|
harnessSessionId: event.sessionId });
|
|
678
706
|
const offered = await nativeActivationHintFor({ adapter, event,
|
|
@@ -809,7 +837,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
809
837
|
runtime = defaultRuntime(), budgetMs = DEFAULT_BUDGET_MS,
|
|
810
838
|
readProcessTable = defaultReadProcessTable,
|
|
811
839
|
probeClientVersion = defaultProbeClientVersion,
|
|
812
|
-
platform = `${process.platform}-${process.arch}
|
|
840
|
+
platform = `${process.platform}-${process.arch}`, includeSessions = false }) {
|
|
813
841
|
const deadline = Date.now() + budgetMs;
|
|
814
842
|
const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
|
|
815
843
|
commitOffers: async () => {} };
|
|
@@ -827,8 +855,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
827
855
|
// below: it starts now, while the store opens. On Windows it is the part
|
|
828
856
|
// of a start that runs another program.
|
|
829
857
|
const tableRead = event.kind !== "sessionStart" || (adapter.client?.command ?? null) === null
|
|
830
|
-
? null :
|
|
831
|
-
Math.min(PROCESS_TABLE_MS, deadline - Date.now() - TABLE_RESERVE_MS)) }));
|
|
858
|
+
? null : readClientTable(readProcessTable, adapter, deadline);
|
|
832
859
|
tableRead?.catch(() => {});
|
|
833
860
|
mark("finding the workspace");
|
|
834
861
|
const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
|
|
@@ -881,11 +908,16 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
881
908
|
};
|
|
882
909
|
delete result.offerInputs;
|
|
883
910
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
911
|
+
// Who is present, for a caller that asks; the hook entry point never does.
|
|
912
|
+
// Reading it took the whole store a second time on every hook, 0.4 s to 4 s
|
|
913
|
+
// of the five-second budget in real workspaces (#298).
|
|
914
|
+
if (includeSessions) {
|
|
915
|
+
mark("reading the status");
|
|
916
|
+
const status = await context.service.collectStatus({
|
|
917
|
+
workspaceId: context.descriptor.id });
|
|
918
|
+
assertHookBudget(deadline);
|
|
919
|
+
result.sessions = status.participants.filter(p => p.presence !== "offline");
|
|
920
|
+
}
|
|
889
921
|
result.service = context.service;
|
|
890
922
|
return result;
|
|
891
923
|
};
|
|
@@ -988,8 +1020,8 @@ export async function registerNativeSession({ adapterId, adapters, event: target
|
|
|
988
1020
|
* register those in this workspace. Bounded by one budget and fail-open; the
|
|
989
1021
|
* answer lists what was registered and is otherwise informational.
|
|
990
1022
|
*/
|
|
991
|
-
export async function registerDiscoveredSessions({ adapters, workspaceId, dataHome,
|
|
992
|
-
budgetMs = 2_500, runtime, probeClientVersion, platform }) {
|
|
1023
|
+
export async function registerDiscoveredSessions({ adapters, loadAdapters, workspaceId, dataHome,
|
|
1024
|
+
env = {}, budgetMs = 2_500, runtime, probeClientVersion, platform }) {
|
|
993
1025
|
const deadline = Date.now() + budgetMs;
|
|
994
1026
|
const remaining = () => Math.max(0, deadline - Date.now());
|
|
995
1027
|
// An adapter that ignores its timeout must not hold status: its answer is
|
|
@@ -1001,19 +1033,27 @@ export async function registerDiscoveredSessions({ adapters, workspaceId, dataHo
|
|
|
1001
1033
|
.finally(() => clearTimeout(timer));
|
|
1002
1034
|
};
|
|
1003
1035
|
const registered = [];
|
|
1004
|
-
let installed;
|
|
1036
|
+
let installed, registry = adapters;
|
|
1005
1037
|
try {
|
|
1006
1038
|
installed = new Set((await loadOwnership({ dataHome })).installs.map(entry => entry.adapterId));
|
|
1007
1039
|
} catch {
|
|
1008
1040
|
return registered;
|
|
1009
1041
|
}
|
|
1010
|
-
|
|
1042
|
+
// A caller that loads adapters on demand loads only the installed ones (#270),
|
|
1043
|
+
// within the same budget: a loader that fails or has not answered when it runs
|
|
1044
|
+
// out leaves nothing to ask, the answer for a client with no adapter.
|
|
1045
|
+
if (typeof loadAdapters === "function") {
|
|
1046
|
+
const loaded = await withinBudget(() => loadAdapters([...installed]));
|
|
1047
|
+
if (Array.isArray(loaded)) return registered;
|
|
1048
|
+
registry = loaded;
|
|
1049
|
+
}
|
|
1050
|
+
for (const adapter of Object.values(registry ?? {})) {
|
|
1011
1051
|
if (typeof adapter?.discoverNativeSessions !== "function" || !installed.has(adapter.id)) continue;
|
|
1012
1052
|
const found = await withinBudget(() => adapter.discoverNativeSessions({ env,
|
|
1013
1053
|
timeoutMs: remaining() }));
|
|
1014
1054
|
for (const candidate of Array.isArray(found) ? found : []) {
|
|
1015
1055
|
if (remaining() === 0) return registered;
|
|
1016
|
-
const result = await registerNativeSession({ adapterId: adapter.id, adapters,
|
|
1056
|
+
const result = await registerNativeSession({ adapterId: adapter.id, adapters: registry,
|
|
1017
1057
|
event: { sessionId: candidate?.sessionId, cwd: candidate?.cwd }, clientPid: candidate?.clientPid,
|
|
1018
1058
|
workspaceId, dataHome, env, budgetMs: remaining(),
|
|
1019
1059
|
...(runtime ? { runtime } : {}), ...(probeClientVersion ? { probeClientVersion } : {}),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/installer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.mjs",
|
|
8
|
-
"./hook-support": "./src/hook-support.mjs"
|
|
8
|
+
"./hook-support": "./src/hook-support.mjs",
|
|
9
|
+
"./live-policy": "./src/live-policy.mjs"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"src/"
|
|
@@ -39,6 +39,7 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
39
39
|
const createdDirectories = await missingArtifactParents({ home: context.home,
|
|
40
40
|
artifacts: operation.artifacts });
|
|
41
41
|
const installContext = { ...context,
|
|
42
|
+
indicator: operation.indicator,
|
|
42
43
|
clientVersion: operation.clientVersion, platform: operation.platform,
|
|
43
44
|
requestedLivePolicy: operation.livePolicy ?? "off",
|
|
44
45
|
livePolicy: operation.configuredLivePolicy ?? operation.effectiveLivePolicy ?? "off" };
|
|
@@ -71,10 +72,12 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
71
72
|
version: operation.clientVersion ?? null, accVersion,
|
|
72
73
|
artifacts: operation.artifacts, createdDirectories,
|
|
73
74
|
deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
|
|
75
|
+
indicator: operation.indicator,
|
|
74
76
|
nativeActivation: native });
|
|
75
77
|
results.operations.push({ ...operation, applied: true,
|
|
76
78
|
needsAction: outcome.needsAction ?? [],
|
|
77
79
|
changes: outcome.changes ?? [], diagnostics: [
|
|
80
|
+
...(operation.indicatorDiagnostic === undefined ? [] : [operation.indicatorDiagnostic]),
|
|
78
81
|
...(operation.deliveryDiagnostic === undefined
|
|
79
82
|
? [] : [operation.deliveryDiagnostic]),
|
|
80
83
|
...(outcome.diagnostics ?? []),
|