agents-can-communicate 0.11.0 → 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/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/STATUS_INDICATOR.md +8 -3
- package/docs/TROUBLESHOOTING.md +2 -2
- 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/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +29 -13
- 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/native-attempt-writer.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +40 -27
- package/node_modules/@agents-can-communicate/cli/package.json +7 -1
- package/node_modules/@agents-can-communicate/cli/src/adapter-catalog.mjs +33 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -3
- package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +2 -2
- package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +4 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +38 -60
- 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/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 +5 -2
- 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/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 +3 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/state-reads.mjs +24 -7
- package/package.json +1 -1
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
import { AccError, CONFIG_FILENAME, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS,
|
|
6
6
|
failure, ok }
|
|
7
7
|
from "@agents-can-communicate/protocol";
|
|
8
|
-
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
9
|
-
import { shellQuote } from "@agents-can-communicate/adapter-sdk";
|
|
10
|
-
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
11
|
-
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
12
8
|
|
|
13
9
|
import { parseArgs, positiveNumber } from "./args.mjs";
|
|
14
10
|
|
|
@@ -16,18 +12,10 @@ import { parseArgs, positiveNumber } from "./args.mjs";
|
|
|
16
12
|
// with the argument already half-applied.
|
|
17
13
|
const usage = message => new AccError(EXIT.USAGE, message);
|
|
18
14
|
import { commandHelpText, describeCommand, describeCommands, helpText } from "./help.mjs";
|
|
19
|
-
import {
|
|
20
|
-
import { runConfigCommand } from "./config-command.mjs";
|
|
21
|
-
import { ALL_ADAPTERS, runInstallCommand } from "./install-command.mjs";
|
|
22
|
-
import { runDoctor } from "./doctor-command.mjs";
|
|
23
|
-
import { runManagementDoctor } from "./managed-runtime/diagnostics.mjs";
|
|
24
|
-
import { runStoreMigration } from "./store-migration-command.mjs";
|
|
15
|
+
import { loadAdapters } from "./adapter-catalog.mjs";
|
|
25
16
|
import { createGitProbe } from "./git-probe.mjs";
|
|
26
|
-
import { runClaimCommand, runReleaseCommand } from "./claim-command.mjs";
|
|
27
17
|
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
28
18
|
import { resolveOwner } from "./session-owner.mjs";
|
|
29
|
-
import { discoverWorkspace } from "./workspace-discovery.mjs";
|
|
30
|
-
import { resolveSavedHookWorkspace } from "./hook-workspace.mjs";
|
|
31
19
|
|
|
32
20
|
const DEFAULT_CADENCE_MS = 30_000;
|
|
33
21
|
|
|
@@ -39,6 +27,10 @@ const DEFAULT_CADENCE_MS = 30_000;
|
|
|
39
27
|
* looking is safe.
|
|
40
28
|
*/
|
|
41
29
|
async function locateContext(options, runtime) {
|
|
30
|
+
// Workspace discovery reads through the store's no-follow opens, so it is
|
|
31
|
+
// loaded by the commands that use a workspace, never by `version` (#270).
|
|
32
|
+
const [{ discoverWorkspace }, { resolveSavedHookWorkspace }] = await Promise.all([
|
|
33
|
+
import("./workspace-discovery.mjs"), import("./hook-workspace.mjs")]);
|
|
42
34
|
const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
|
|
43
35
|
env: runtime.env });
|
|
44
36
|
const discovery = {
|
|
@@ -59,6 +51,10 @@ async function locateContext(options, runtime) {
|
|
|
59
51
|
}
|
|
60
52
|
|
|
61
53
|
async function withService({ descriptor, paths, dataHome }, runtime) {
|
|
54
|
+
// Loaded here rather than at the top: `version`, `help` and the commands
|
|
55
|
+
// that work on a machine never open a store (#270).
|
|
56
|
+
const [{ openFilesystemStore }, { createCoordinationService }] = await Promise.all([
|
|
57
|
+
import("@agents-can-communicate/storage-filesystem"), import("@agents-can-communicate/core")]);
|
|
62
58
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
63
59
|
ids: runtime.ids, workspaceId: descriptor.id });
|
|
64
60
|
const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
@@ -120,35 +116,10 @@ function obligationFor(kind, explicit, addressed) {
|
|
|
120
116
|
return obligation;
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
? `sent a wake to ${item.recipientParticipantId} via ${item.transport}, which its session holds `
|
|
128
|
-
+ "for approval because it bypasses permission prompts; the message arrives with its next turn"
|
|
129
|
-
: item.outcome === "woken"
|
|
130
|
-
? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
|
|
131
|
-
: item.outcome === "queued"
|
|
132
|
-
? item.errorCode === "transport_permission_denied"
|
|
133
|
-
? "live offer blocked by sender permissions; message remains queued; retry the same command "
|
|
134
|
-
+ "through approved execution"
|
|
135
|
-
+ (typeof message.clientMessageId === "string"
|
|
136
|
-
? ` with --client-message-id ${shellQuote(message.clientMessageId)}` : " with its original retry key")
|
|
137
|
-
: item.errorCode === "no_live_transport"
|
|
138
|
-
? `${item.recipientParticipantId} has no live transport`
|
|
139
|
-
+ `${item.nativeReasonCode ? ` (${describeNativeReason(item.nativeReasonCode,
|
|
140
|
-
{ launchOption: item.nativeLaunchOption })})` : ""}`
|
|
141
|
-
+ "; the message waits in its inbox"
|
|
142
|
-
: item.errorCode === "recipient_offline" && item.endReason === "clear"
|
|
143
|
-
? `${item.recipientParticipantId}'s conversation was cleared (/clear); the message waits `
|
|
144
|
-
+ "until that conversation resumes"
|
|
145
|
-
: item.errorCode === "recipient_offline"
|
|
146
|
-
? `${item.recipientParticipantId} has no open session; the message waits until it starts `
|
|
147
|
-
+ "or resumes one"
|
|
148
|
-
: `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
|
|
149
|
-
: `delivery already ${item.outcome}`);
|
|
150
|
-
return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
|
|
151
|
-
}
|
|
119
|
+
// What a sent message met, in words. Its diagnostics load with the first
|
|
120
|
+
// message a command sends, not with `acc version` (#270).
|
|
121
|
+
const describeRecorded = async (message, delivery) =>
|
|
122
|
+
(await import("./recorded-text.mjs")).recordedText(message, delivery);
|
|
152
123
|
|
|
153
124
|
export async function recordAndOffer({ record, router, selectMessage = value => value }) {
|
|
154
125
|
const recorded = await record();
|
|
@@ -179,7 +150,7 @@ export async function recordAndOffer({ record, router, selectMessage = value =>
|
|
|
179
150
|
* A live binding wakes on the router's own rule: a current lease, or a lapsed
|
|
180
151
|
* one the adapter can re-verify. Without that the next send is durable.
|
|
181
152
|
*/
|
|
182
|
-
export function presenceBreakdown(participants = [], adapters =
|
|
153
|
+
export function presenceBreakdown(participants = [], adapters = []) {
|
|
183
154
|
const nextTurn = new Set(adapters.filter(adapter => adapter.capabilities?.delivery?.nextTurn === true)
|
|
184
155
|
.map(adapter => adapter.id));
|
|
185
156
|
const refreshes = new Set(adapters.filter(adapter => typeof adapter.refreshNativeSession === "function")
|
|
@@ -212,13 +183,13 @@ export function describePresence({ live = 0, stale = 0 } = {},
|
|
|
212
183
|
return `${live} live (${parts.join("; ")})`;
|
|
213
184
|
}
|
|
214
185
|
|
|
215
|
-
export function describeStatus(status) {
|
|
186
|
+
export function describeStatus(status, adapters = []) {
|
|
216
187
|
// `live` counts everyone present, which includes sessions gone stale - a
|
|
217
188
|
// client that exited without its session being closed keeps a record that
|
|
218
189
|
// stops being answered but does not disappear. Printing the number alone
|
|
219
190
|
// said "1 live" about a workspace where the last agent had left minutes
|
|
220
191
|
// before, which is the one thing a person reads this line to find out.
|
|
221
|
-
const line = `${describePresence(status.counts, presenceBreakdown(status.participants))}; `
|
|
192
|
+
const line = `${describePresence(status.counts, presenceBreakdown(status.participants, adapters))}; `
|
|
222
193
|
+ `${status.counts.claims} claim(s); `
|
|
223
194
|
+ `protection ${status.protection}`;
|
|
224
195
|
// A transport accepting a message is not a model reading it. Said only when
|
|
@@ -294,8 +265,8 @@ const HANDLERS = Object.freeze({
|
|
|
294
265
|
return { data: intent, text: `intent: ${intent.summary}` };
|
|
295
266
|
},
|
|
296
267
|
|
|
297
|
-
claim: runClaimCommand,
|
|
298
|
-
release: runReleaseCommand,
|
|
268
|
+
claim: async input => (await import("./claim-command.mjs")).runClaimCommand(input),
|
|
269
|
+
release: async input => (await import("./claim-command.mjs")).runReleaseCommand(input),
|
|
299
270
|
|
|
300
271
|
message: async ({ options, context }) => {
|
|
301
272
|
const kind = options.type ?? "note";
|
|
@@ -310,7 +281,7 @@ const HANDLERS = Object.freeze({
|
|
|
310
281
|
subject: options.subject, body: options.body, descriptor: context.descriptor }) });
|
|
311
282
|
const message = routed.recorded;
|
|
312
283
|
return { data: { message, delivery: routed.delivery },
|
|
313
|
-
text:
|
|
284
|
+
text: await describeRecorded(message, routed.delivery) };
|
|
314
285
|
},
|
|
315
286
|
|
|
316
287
|
inbox: async ({ options, context }) => {
|
|
@@ -337,7 +308,7 @@ const HANDLERS = Object.freeze({
|
|
|
337
308
|
subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
|
|
338
309
|
const result = routed.recorded;
|
|
339
310
|
return { data: { message: result.reply, receipt: result.receipt, delivery: routed.delivery },
|
|
340
|
-
text: `${
|
|
311
|
+
text: `${await describeRecorded(result.reply, routed.delivery)}; acknowledged ${result.receipt.messageId}` };
|
|
341
312
|
},
|
|
342
313
|
|
|
343
314
|
request: async ({ options, context }) => {
|
|
@@ -355,7 +326,7 @@ const HANDLERS = Object.freeze({
|
|
|
355
326
|
}) });
|
|
356
327
|
const message = routed.recorded;
|
|
357
328
|
return { data: { message, delivery: routed.delivery },
|
|
358
|
-
text:
|
|
329
|
+
text: await describeRecorded(message, routed.delivery) };
|
|
359
330
|
},
|
|
360
331
|
|
|
361
332
|
ack: async ({ options, context }) => {
|
|
@@ -374,17 +345,20 @@ const HANDLERS = Object.freeze({
|
|
|
374
345
|
remaining: options.remaining ?? [], blockers: options.blocker ?? [] }) });
|
|
375
346
|
const handoff = routed.recorded;
|
|
376
347
|
return { data: { message: handoff.message, delivery: routed.delivery },
|
|
377
|
-
text:
|
|
348
|
+
text: await describeRecorded(handoff.message, routed.delivery) };
|
|
378
349
|
},
|
|
379
350
|
|
|
380
351
|
status: async ({ options, context, runtime }) => {
|
|
381
352
|
await registerNativeSessions(runtime, context);
|
|
382
353
|
const status = await context.service.collectStatus({
|
|
383
354
|
participantId: options.participant, all: options.all === true });
|
|
384
|
-
|
|
355
|
+
// Only the clients present here say how their idle sessions are reached.
|
|
356
|
+
const adapters = await loadAdapters((status.participants ?? []).map(item => item.harness));
|
|
357
|
+
return { data: status, text: describeStatus(status, Object.values(adapters)) };
|
|
385
358
|
},
|
|
386
359
|
|
|
387
360
|
config: async ({ options, runtime }) => {
|
|
361
|
+
const { runConfigCommand } = await import("./config-command.mjs");
|
|
388
362
|
const result = await runConfigCommand({ subcommand: options.subcommand,
|
|
389
363
|
cwd: options.cwd ?? runtime.cwd,
|
|
390
364
|
// A pipe is not a person. Without a TTY there is nobody to answer, so the
|
|
@@ -411,11 +385,11 @@ const HANDLERS = Object.freeze({
|
|
|
411
385
|
text: result.written ? `wrote ${result.file}` : `not written: ${result.file}` };
|
|
412
386
|
},
|
|
413
387
|
|
|
414
|
-
install: async ({ options, runtime }) =>
|
|
415
|
-
runInstallCommand({ options, runtime, action: "install" }),
|
|
388
|
+
install: async ({ options, runtime }) => (await import("./install-command.mjs"))
|
|
389
|
+
.runInstallCommand({ options, runtime, action: "install" }),
|
|
416
390
|
|
|
417
|
-
uninstall: async ({ options, runtime }) =>
|
|
418
|
-
runInstallCommand({ options, runtime, action: "uninstall" }),
|
|
391
|
+
uninstall: async ({ options, runtime }) => (await import("./install-command.mjs"))
|
|
392
|
+
.runInstallCommand({ options, runtime, action: "uninstall" }),
|
|
419
393
|
|
|
420
394
|
prune: async ({ options, context }) => {
|
|
421
395
|
const result = await context.service.prune({
|
|
@@ -435,9 +409,12 @@ const HANDLERS = Object.freeze({
|
|
|
435
409
|
return { data: result, text };
|
|
436
410
|
},
|
|
437
411
|
|
|
438
|
-
doctor: async input => input.options.migrateStore
|
|
412
|
+
doctor: async input => input.options.migrateStore
|
|
413
|
+
? (await import("./store-migration-command.mjs")).runStoreMigration(input)
|
|
414
|
+
: (await import("./doctor-command.mjs")).runDoctor(input),
|
|
439
415
|
|
|
440
|
-
update: async ({ options, runtime }) =>
|
|
416
|
+
update: async ({ options, runtime }) => (await import("./update-command.mjs"))
|
|
417
|
+
.runUpdateCommand({ options, runtime }),
|
|
441
418
|
|
|
442
419
|
help: async ({ options }) => {
|
|
443
420
|
if (options.helpCommand === undefined) {
|
|
@@ -499,8 +476,9 @@ export async function main(argv, runtime) {
|
|
|
499
476
|
const options = context === null ? parsed.options
|
|
500
477
|
: await resolveOwner({ command: parsed.command, options: parsed.options,
|
|
501
478
|
context, env: runtime.env });
|
|
502
|
-
const
|
|
503
|
-
|
|
479
|
+
const handler = managementDoctor
|
|
480
|
+
? (await import("./managed-runtime/diagnostics.mjs")).runManagementDoctor : HANDLERS[parsed.command];
|
|
481
|
+
const { data, text, error: outcome } = await handler({ options, context, runtime });
|
|
504
482
|
// A handler may have done real work and still failed: `acc install` writes
|
|
505
483
|
// for the clients it could and reports the one it could not. The data is
|
|
506
484
|
// printed either way, and the command still fails.
|
|
@@ -99,7 +99,7 @@ export async function updateNativeSessions(adapters, { service, status, root, no
|
|
|
99
99
|
const failures = {
|
|
100
100
|
transport_permission_denied: "this command cannot access the local delivery channel; "
|
|
101
101
|
+ "check the sender's command permissions",
|
|
102
|
-
client_process_unknown: "the client process could not be identified;
|
|
102
|
+
client_process_unknown: "the client process could not be identified; the next prompt looks for it again",
|
|
103
103
|
handshake_failed: "the session handshake failed; check the client's integration/channel setup",
|
|
104
104
|
handshake_timeout: "the session handshake timed out; check the local channel and retry on the next turn",
|
|
105
105
|
// Since the contract gate shipped, this reason means the version now serving
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { shellQuote } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
3
|
+
|
|
4
|
+
export function recordedText(message, delivery) {
|
|
5
|
+
const diagnostics = delivery.map(item => item.outcome === "offered"
|
|
6
|
+
? `live offered to ${item.recipientParticipantId} via ${item.transport}`
|
|
7
|
+
: item.outcome === "woken" && item.pendingApproval === true
|
|
8
|
+
? `sent a wake to ${item.recipientParticipantId} via ${item.transport}, which its session holds `
|
|
9
|
+
+ "for approval because it bypasses permission prompts; the message arrives with its next turn"
|
|
10
|
+
: item.outcome === "woken"
|
|
11
|
+
? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
|
|
12
|
+
: item.outcome === "queued"
|
|
13
|
+
? item.errorCode === "transport_permission_denied"
|
|
14
|
+
? "live offer blocked by sender permissions; message remains queued; retry the same command "
|
|
15
|
+
+ "through approved execution"
|
|
16
|
+
+ (typeof message.clientMessageId === "string"
|
|
17
|
+
? ` with --client-message-id ${shellQuote(message.clientMessageId)}` : " with its original retry key")
|
|
18
|
+
: item.errorCode === "no_live_transport"
|
|
19
|
+
? `${item.recipientParticipantId} has no live transport`
|
|
20
|
+
+ `${item.nativeReasonCode ? ` (${describeNativeReason(item.nativeReasonCode,
|
|
21
|
+
{ launchOption: item.nativeLaunchOption })})` : ""}`
|
|
22
|
+
+ "; the message waits in its inbox"
|
|
23
|
+
: item.errorCode === "recipient_offline" && item.endReason === "clear"
|
|
24
|
+
? `${item.recipientParticipantId}'s conversation was cleared (/clear); the message waits `
|
|
25
|
+
+ "until that conversation resumes"
|
|
26
|
+
: item.errorCode === "recipient_offline"
|
|
27
|
+
? `${item.recipientParticipantId} has no open session; the message waits until it starts `
|
|
28
|
+
+ "or resumes one"
|
|
29
|
+
: `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
|
|
30
|
+
: `delivery already ${item.outcome}`);
|
|
31
|
+
return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
|
|
32
|
+
}
|
|
@@ -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,
|
|
@@ -45,9 +45,12 @@ function safeTransport(value, opaqueEndpointRef) {
|
|
|
45
45
|
return opaqueEndpointRef === "live-adapter" ? "native-live" : "live-adapter";
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
48
|
+
export function createDeliveryRouter({ service, adapters, loadAdapter, clock, readLivePolicy,
|
|
49
49
|
readNativeReason = input => lastNativeReason({ runtimeDir: service.store?.root, ...input }) }) {
|
|
50
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);
|
|
51
54
|
const observeOffer = (binding, reasonCode = null) => {
|
|
52
55
|
// Capture time and order now, but never await optional diagnostic I/O on
|
|
53
56
|
// the delivery path. The SDK bounds its own lifetime for short-lived CLIs.
|
|
@@ -119,7 +122,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
119
122
|
// that bound no live transport. It reads its inbox, so it is not gone.
|
|
120
123
|
if (bindings.length === 0) return withoutLiveTransport(participantId, target);
|
|
121
124
|
const evaluated = await Promise.all(bindings.map(async binding => {
|
|
122
|
-
const adapter =
|
|
125
|
+
const adapter = await adapterFor(binding.adapterId);
|
|
123
126
|
return { binding, adapter, policy: await policyFor(adapter, binding) };
|
|
124
127
|
}));
|
|
125
128
|
const permitted = evaluated.filter(({ policy }) => permits(policy, message.kind));
|
|
@@ -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.11.
|
|
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/"
|