agents-can-communicate 0.1.18 → 0.2.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 +78 -70
- package/SECURITY.md +31 -0
- package/bin/acc-hook.mjs +94 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +6 -1
- package/docs/ADAPTER_AUTHORING.md +172 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +102 -214
- package/docs/CLI.md +157 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +143 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +109 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +102 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +42 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +12 -4
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +18 -11
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
- 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 +20 -22
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +9 -9
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +20 -22
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +11 -30
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +9 -2
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +11 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +115 -63
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
- package/package.json +19 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { clearSessionBinding, loadSessionBinding, storeSessionBinding }
|
|
5
|
+
import { clearSessionBinding, effectiveCapabilities, loadSessionBinding, storeSessionBinding }
|
|
6
6
|
from "@agents-can-communicate/adapter-sdk";
|
|
7
7
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
8
8
|
import { createId } from "@agents-can-communicate/protocol";
|
|
@@ -11,6 +11,7 @@ import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
|
11
11
|
from "@agents-can-communicate/cli";
|
|
12
12
|
|
|
13
13
|
import { resolveClientPid } from "./client-pid.mjs";
|
|
14
|
+
import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
|
|
14
15
|
import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
|
|
15
16
|
|
|
16
17
|
// Kept cohesive above 300 lines because every handler shares one fail-open
|
|
@@ -22,6 +23,28 @@ import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs
|
|
|
22
23
|
// let a call through than to make someone's session sit waiting on us.
|
|
23
24
|
const DEFAULT_BUDGET_MS = 5_000;
|
|
24
25
|
|
|
26
|
+
const byteLength = value => Buffer.byteLength(value, "utf8");
|
|
27
|
+
|
|
28
|
+
function compactInboxRecovery(messages) {
|
|
29
|
+
const first = messages[0].messageId;
|
|
30
|
+
const rest = messages.length > 1 ? ` (+${messages.length - 1} more in \`acc inbox\`)` : "";
|
|
31
|
+
return `ACC: read pending peer message: \`acc inbox --message ${first}\`${rest}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fitDegradation(projection, visibleDegradation, messages, budgetBytes) {
|
|
35
|
+
const full = [projection, visibleDegradation].filter(Boolean).join("\n");
|
|
36
|
+
if (byteLength(full) <= budgetBytes) return full;
|
|
37
|
+
const recovery = compactInboxRecovery(messages);
|
|
38
|
+
const exactRecovery = compactInboxRecovery(messages.slice(0, 1));
|
|
39
|
+
const withRecovery = [projection, recovery].filter(Boolean).join("\n");
|
|
40
|
+
if (byteLength(withRecovery) <= budgetBytes) return withRecovery;
|
|
41
|
+
const withExactRecovery = [projection, exactRecovery].filter(Boolean).join("\n");
|
|
42
|
+
if (byteLength(withExactRecovery) <= budgetBytes) return withExactRecovery;
|
|
43
|
+
if (byteLength(recovery) <= budgetBytes) return recovery;
|
|
44
|
+
if (byteLength(exactRecovery) <= budgetBytes) return exactRecovery;
|
|
45
|
+
return projection;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
// Declared by this process on the session it opens, so peers can tell an idle
|
|
26
49
|
// session from a dead one. Only one of the four clients fires a heartbeat event,
|
|
27
50
|
// so the rest refresh here: on every turn, and during a long one whenever the
|
|
@@ -163,8 +186,18 @@ async function openContext({ cwd, dataHome, runtime, env }) {
|
|
|
163
186
|
|
|
164
187
|
const HANDLERS = {
|
|
165
188
|
async sessionStart({ event, context, adapter, adapterId, binding, paths,
|
|
166
|
-
readProcessTable }) {
|
|
167
|
-
|
|
189
|
+
readProcessTable, probeClientVersion, platform, deadline }) {
|
|
190
|
+
// A repeated start refreshes the client's version/platform. Remove the old
|
|
191
|
+
// certified facts before any probe, PID lookup, resume, or open can fail;
|
|
192
|
+
// keep only the generation identity needed for a successful resume.
|
|
193
|
+
if (binding !== null) {
|
|
194
|
+
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
195
|
+
accSessionId: binding.accSessionId, generation: binding.generation });
|
|
196
|
+
}
|
|
197
|
+
const clientVersion = await probeClientVersion(adapter,
|
|
198
|
+
{ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
|
|
199
|
+
const clientFacts = { clientVersion, platform };
|
|
200
|
+
const capabilities = effectiveCapabilities(adapter, clientFacts);
|
|
168
201
|
// Once per session, never per turn. A client that cannot be found yields
|
|
169
202
|
// null, and the session is then judged by age alone - which is exactly the
|
|
170
203
|
// behaviour every session had before this existed.
|
|
@@ -186,7 +219,10 @@ const HANDLERS = {
|
|
|
186
219
|
...metadata,
|
|
187
220
|
});
|
|
188
221
|
if (resumed !== null) {
|
|
189
|
-
|
|
222
|
+
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
223
|
+
accSessionId: resumed.sessionId, generation: resumed.generation, ...clientFacts });
|
|
224
|
+
return { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
225
|
+
...clientFacts, capabilities };
|
|
190
226
|
}
|
|
191
227
|
}
|
|
192
228
|
const session = await context.service.openSession({
|
|
@@ -204,8 +240,9 @@ const HANDLERS = {
|
|
|
204
240
|
descriptor: context.descriptor,
|
|
205
241
|
});
|
|
206
242
|
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
207
|
-
accSessionId: session.sessionId, generation: session.generation });
|
|
208
|
-
return { accSessionId: session.sessionId, generation: session.generation
|
|
243
|
+
accSessionId: session.sessionId, generation: session.generation, ...clientFacts });
|
|
244
|
+
return { accSessionId: session.sessionId, generation: session.generation,
|
|
245
|
+
...clientFacts, capabilities };
|
|
209
246
|
},
|
|
210
247
|
|
|
211
248
|
async heartbeat({ binding, context }) {
|
|
@@ -223,7 +260,7 @@ const HANDLERS = {
|
|
|
223
260
|
return {};
|
|
224
261
|
},
|
|
225
262
|
|
|
226
|
-
async beforeTurn({ binding, context, adapter }) {
|
|
263
|
+
async beforeTurn({ binding, context, adapter, adapterId }) {
|
|
227
264
|
if (binding === null) return {};
|
|
228
265
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
229
266
|
// this runs in front of somebody's prompt.
|
|
@@ -239,13 +276,14 @@ const HANDLERS = {
|
|
|
239
276
|
.find(participant => participant.sessionId === binding.accSessionId);
|
|
240
277
|
|
|
241
278
|
// What peers have said to this participant and no model has been shown yet.
|
|
242
|
-
// Without this the projector's peer block never
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
const
|
|
279
|
+
// Without this the projector's peer block never runs in production: an
|
|
280
|
+
// agent sees only the obligation attention line, not the durable body that
|
|
281
|
+
// may be offered after the stdout transport succeeds.
|
|
282
|
+
const delivery = await context.service.nextTurnDelivery({
|
|
246
283
|
workspaceId: context.descriptor.id,
|
|
247
284
|
participantId: mine?.participantId,
|
|
248
285
|
exceptSessionId: binding.accSessionId });
|
|
286
|
+
const messages = delivery.queuedMessages;
|
|
249
287
|
|
|
250
288
|
// Solo costs nothing: nothing to say means nothing printed, not a banner
|
|
251
289
|
// announcing that nobody else is here. But something already said to you is
|
|
@@ -257,8 +295,12 @@ const HANDLERS = {
|
|
|
257
295
|
// The ceiling a team agreed on in `acc.workspace.json`, or the default when
|
|
258
296
|
// there is no config. Validated by the protocol and, until now, never read:
|
|
259
297
|
// the projector was always called with its own default.
|
|
260
|
-
const
|
|
261
|
-
const
|
|
298
|
+
const effective = effectiveCapabilities(adapter, binding);
|
|
299
|
+
const hasStructuredRenderer = typeof adapter.renderContextResult === "function";
|
|
300
|
+
const canOfferNextTurn = effective.delivery.nextTurn === true && hasStructuredRenderer;
|
|
301
|
+
const projectionInput = { ...sync, messages: canOfferNextTurn ? messages : [],
|
|
302
|
+
liveOfferedMessageIds: delivery.liveOfferedMessageIds,
|
|
303
|
+
roomMessageIds: delivery.roomMessageIds,
|
|
262
304
|
currentParticipantId: mine?.participantId };
|
|
263
305
|
const projectionOptions = {
|
|
264
306
|
budgetBytes: context.descriptor.policy?.contextBudgetBytes };
|
|
@@ -266,64 +308,49 @@ const HANDLERS = {
|
|
|
266
308
|
// another message's visible header, so only projector metadata proves
|
|
267
309
|
// which complete groups survived the byte budget. A custom adapter without
|
|
268
310
|
// metadata may still inject text, but cannot advance a receipt from it.
|
|
269
|
-
const projection = !
|
|
311
|
+
const projection = !hasStructuredRenderer
|
|
270
312
|
? { text: await adapter.renderContext?.(projectionInput, projectionOptions) ?? "",
|
|
271
|
-
|
|
313
|
+
offeredMessageIds: [], includedAttentionIds: [] }
|
|
272
314
|
: await adapter.renderContextResult(projectionInput, projectionOptions);
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
315
|
+
const clientFactsKnown = typeof binding.clientVersion === "string"
|
|
316
|
+
&& typeof binding.platform === "string";
|
|
317
|
+
const reason = !effective.delivery.nextTurn
|
|
318
|
+
? clientFactsKnown
|
|
319
|
+
? `client ${binding.clientVersion} on ${binding.platform} is not certified for nextTurn`
|
|
320
|
+
: "the client version or platform is unknown"
|
|
321
|
+
: !hasStructuredRenderer ? "this adapter lacks structured delivery metadata" : null;
|
|
322
|
+
const degradation = reason !== null && messages.length > 0
|
|
323
|
+
? `acc: ${messages.length} pending message(s) withheld because ${reason}; read `
|
|
324
|
+
+ `${messages[0].messageId} with acc inbox --message ${messages[0].messageId}` : null;
|
|
278
325
|
const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
|
|
279
|
-
const
|
|
280
|
-
const projected =
|
|
281
|
-
|
|
326
|
+
const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
|
|
327
|
+
const projected = degradation === null ? projection.text
|
|
328
|
+
: fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
|
|
282
329
|
if (projected === "") {
|
|
283
330
|
return degradation === null ? { stdout: "" } : { stdout: "", stderr: degradation };
|
|
284
331
|
}
|
|
285
332
|
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
recipientParticipantId: mine.participantId, state: "injected" })
|
|
297
|
-
.catch(error => failures.push(`${message.messageId}: ${error.message}`));
|
|
298
|
-
}
|
|
299
|
-
// A note carries no ack obligation, so after its one full showing it leaves
|
|
300
|
-
// a single low-priority `unread_note` breadcrumb. Advancing that receipt
|
|
301
|
-
// injected -> seen the turn the breadcrumb is shown is what makes it
|
|
302
|
-
// one-shot: next turn the note reads `seen`, the breadcrumb stays quiet, and
|
|
303
|
-
// a delivered decision is recoverable without becoming a standing nag - the
|
|
304
|
-
// noise a reader learns to skip. Only what was actually shown is advanced,
|
|
305
|
-
// for the same reason the loop above only records what fit.
|
|
306
|
-
const includedAttention = new Set(projection.includedAttentionIds ?? []);
|
|
307
|
-
for (const item of sync.attention ?? []) {
|
|
308
|
-
if (item.kind !== "unread_note") continue;
|
|
309
|
-
if (!includedAttention.has(item.sourceId)) continue;
|
|
310
|
-
await context.service.markDelivery({ sessionId: binding.accSessionId,
|
|
311
|
-
generation: binding.generation, messageId: item.sourceId,
|
|
312
|
-
recipientParticipantId: mine.participantId, state: "seen" })
|
|
313
|
-
.catch(error => failures.push(`${item.sourceId}: ${error.message}`));
|
|
314
|
-
}
|
|
333
|
+
// The renderer returns ids as metadata, never as text to parse. A peer body
|
|
334
|
+
// can imitate every visible label, so only a complete group selected by the
|
|
335
|
+
// projector is eligible for the post-write offer commit.
|
|
336
|
+
const offered = new Set(canOfferNextTurn ? projection.offeredMessageIds ?? [] : []);
|
|
337
|
+
const offerInputs = messages.filter(message => offered.has(message.messageId))
|
|
338
|
+
.map(message => ({ messageId: message.messageId,
|
|
339
|
+
recipientParticipantId: mine.participantId,
|
|
340
|
+
targetSessionId: binding.accSessionId, targetGeneration: binding.generation,
|
|
341
|
+
transport: "next-turn", adapterId,
|
|
342
|
+
clientVersion: binding.clientVersion }));
|
|
315
343
|
// Same again: Kimi Code shows the model a hook's raw stdout, while Gemini
|
|
316
344
|
// and Claude Code want an envelope and drop a bare string.
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
345
|
+
// The entry point owns the transport boundary. This handler only prepares
|
|
346
|
+
// offer inputs; recording them here would claim delivery before stdout's
|
|
347
|
+
// callback proves that the bytes crossed.
|
|
320
348
|
const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
|
|
321
|
-
|
|
349
|
+
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
350
|
+
if (degradation === null) return { ...outcome, offerInputs: writableOffers };
|
|
322
351
|
return { ...outcome,
|
|
323
|
-
stderr: [outcome.stderr, degradation,
|
|
324
|
-
|
|
325
|
-
: `acc: delivery not recorded for ${failures.join(", ")}`]
|
|
326
|
-
.filter(Boolean).join("\n") };
|
|
352
|
+
stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
|
|
353
|
+
offerInputs: writableOffers };
|
|
327
354
|
},
|
|
328
355
|
|
|
329
356
|
async beforeTool({ binding, context, event, adapter }) {
|
|
@@ -394,8 +421,12 @@ const HANDLERS = {
|
|
|
394
421
|
*/
|
|
395
422
|
export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
396
423
|
runtime = defaultRuntime(), budgetMs = DEFAULT_BUDGET_MS,
|
|
397
|
-
readProcessTable = defaultReadProcessTable
|
|
398
|
-
|
|
424
|
+
readProcessTable = defaultReadProcessTable,
|
|
425
|
+
probeClientVersion = defaultProbeClientVersion,
|
|
426
|
+
platform = `${process.platform}-${process.arch}` }) {
|
|
427
|
+
const deadline = Date.now() + budgetMs;
|
|
428
|
+
const result = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
|
|
429
|
+
commitOffers: async () => {} };
|
|
399
430
|
let timer = null;
|
|
400
431
|
try {
|
|
401
432
|
const adapter = adapters?.[adapterId];
|
|
@@ -410,7 +441,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
410
441
|
const work = handler === undefined
|
|
411
442
|
? Promise.resolve({})
|
|
412
443
|
: handler({ event, context, adapter, adapterId, binding, paths: context.paths,
|
|
413
|
-
readProcessTable });
|
|
444
|
+
readProcessTable, probeClientVersion, platform, deadline });
|
|
414
445
|
|
|
415
446
|
// The loser of a race is not cancelled, so the timer is cleared explicitly:
|
|
416
447
|
// an outstanding one keeps the process alive long past its answer.
|
|
@@ -419,6 +450,24 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
419
450
|
});
|
|
420
451
|
Object.assign(result, await Promise.race([work, budget]));
|
|
421
452
|
|
|
453
|
+
const offerInputs = result.offerInputs ?? [];
|
|
454
|
+
let commitPromise = null;
|
|
455
|
+
result.commitOffers = () => {
|
|
456
|
+
if (commitPromise !== null) return commitPromise;
|
|
457
|
+
commitPromise = (async () => {
|
|
458
|
+
for (const input of offerInputs) {
|
|
459
|
+
const remaining = deadline - Date.now();
|
|
460
|
+
if (remaining <= 0) throw new Error("hook budget exhausted before offer commit");
|
|
461
|
+
// The durable transaction owns deadline cancellation. Racing it here
|
|
462
|
+
// would only reject the public promise while the losing writer kept
|
|
463
|
+
// waiting and could publish later.
|
|
464
|
+
await context.service.recordOfferSucceeded({ ...input, deadlineAt: deadline });
|
|
465
|
+
}
|
|
466
|
+
})();
|
|
467
|
+
return commitPromise;
|
|
468
|
+
};
|
|
469
|
+
delete result.offerInputs;
|
|
470
|
+
|
|
422
471
|
const status = await context.service.collectStatus({
|
|
423
472
|
workspaceId: context.descriptor.id });
|
|
424
473
|
result.sessions = status.participants.filter(p => p.presence !== "offline");
|
|
@@ -428,6 +477,9 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
428
477
|
result.reason = error.message;
|
|
429
478
|
result.decision = "allow";
|
|
430
479
|
result.stdout = "";
|
|
480
|
+
// A later failure may happen after a turn prepared offer inputs. Once the
|
|
481
|
+
// fail-open path withdraws stdout, no transport boundary remains to commit.
|
|
482
|
+
result.commitOffers = async () => {};
|
|
431
483
|
} finally {
|
|
432
484
|
if (timer !== null) clearTimeout(timer);
|
|
433
485
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
2
|
+
removeEmptyOwnedDirectories, removeOwnedArtifacts } from "./ownership.mjs";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Carry out a plan, one adapter at a time.
|
|
@@ -30,26 +31,41 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
33
|
if (plan.action === "install") {
|
|
33
|
-
const
|
|
34
|
+
const createdDirectories = await missingArtifactParents({ home: context.home,
|
|
35
|
+
artifacts: operation.artifacts });
|
|
36
|
+
const installContext = { ...context,
|
|
37
|
+
requestedLivePolicy: operation.livePolicy ?? "off",
|
|
38
|
+
livePolicy: operation.effectiveLivePolicy ?? "off" };
|
|
39
|
+
const outcome = await adapter.install(installContext);
|
|
34
40
|
// Recorded after the write, so a record never claims an install that
|
|
35
41
|
// did not happen. The reverse order would leave uninstall trying to
|
|
36
42
|
// remove files nothing created.
|
|
37
43
|
await recordInstall({ dataHome, adapterId: adapter.id,
|
|
38
44
|
version: operation.clientVersion ?? null, accVersion,
|
|
39
|
-
artifacts: operation.artifacts });
|
|
45
|
+
artifacts: operation.artifacts, createdDirectories });
|
|
40
46
|
results.operations.push({ ...operation, applied: true,
|
|
41
|
-
changes: outcome.changes ?? [], diagnostics:
|
|
47
|
+
changes: outcome.changes ?? [], diagnostics: [
|
|
48
|
+
...(operation.deliveryDiagnostic === undefined
|
|
49
|
+
? [] : [operation.deliveryDiagnostic]),
|
|
50
|
+
...(outcome.diagnostics ?? []),
|
|
51
|
+
] });
|
|
42
52
|
} else {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
53
|
+
// Keep the record until every cleanup step succeeds. It is both the
|
|
54
|
+
// authority for deletion and the only durable recipe a retry has when
|
|
55
|
+
// the client or one of ACC's own artifacts is already gone.
|
|
56
|
+
const owned = await removeOwnedArtifacts({ dataHome, adapterId: adapter.id });
|
|
46
57
|
// What ownership held back is passed on, because the adapter would
|
|
47
58
|
// otherwise remove its own layout unconditionally and undo the decision.
|
|
48
59
|
// The case that matters: someone put their own work inside a directory
|
|
49
60
|
// ACC created, and a recognised path is not a reason to delete it.
|
|
50
61
|
const outcome = await adapter.uninstall({ ...context, keep: owned.kept });
|
|
62
|
+
const directories = await removeEmptyOwnedDirectories({ home: context.home,
|
|
63
|
+
directories: owned.createdDirectories });
|
|
64
|
+
await finalizeRemoval({ dataHome, adapterId: adapter.id });
|
|
51
65
|
results.operations.push({ ...operation, applied: true,
|
|
52
66
|
changes: outcome.changes ?? [], removed: owned.removed, kept: owned.kept,
|
|
67
|
+
removedDirectories: directories.removed, keptDirectories: directories.kept,
|
|
68
|
+
missingDirectories: directories.missing,
|
|
53
69
|
diagnostics: outcome.diagnostics ?? [] });
|
|
54
70
|
}
|
|
55
71
|
} catch (error) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
|
+
import { effectiveCapabilities } from "@agents-can-communicate/adapter-sdk";
|
|
5
|
+
|
|
4
6
|
const run = promisify(execFile);
|
|
5
7
|
|
|
6
8
|
const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
|
|
@@ -9,7 +11,7 @@ const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
|
|
|
9
11
|
// "0.36.1", a banner with the number somewhere inside. The number is extracted
|
|
10
12
|
// where it can be, and the raw line is kept either way - "present, version
|
|
11
13
|
// unreadable" is a real state and hiding it would make the client look absent.
|
|
12
|
-
const VERSION =
|
|
14
|
+
const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b|$)/;
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Ask the operating system what a client reports as its version.
|
|
@@ -36,7 +38,8 @@ const withTimeout = (work, ms, label) => new Promise((resolve, reject) => {
|
|
|
36
38
|
* letting it throw would hide the other three behind it.
|
|
37
39
|
*/
|
|
38
40
|
export async function detectInstallation({ adapters, context, probe = spawnProbe,
|
|
39
|
-
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS
|
|
41
|
+
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
|
42
|
+
platform = `${process.platform}-${process.arch}` }) {
|
|
40
43
|
const entries = await Promise.all([...adapters]
|
|
41
44
|
// Ordered by id so two runs can be diffed, and so a plan built from this is
|
|
42
45
|
// deterministic rather than dependent on registry order.
|
|
@@ -44,8 +47,8 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
44
47
|
.map(async adapter => {
|
|
45
48
|
const entry = { adapterId: adapter.id, displayName: adapter.displayName,
|
|
46
49
|
present: false, version: null, versionOutput: null, installed: false,
|
|
47
|
-
diagnostics: [], needsAction: [], capabilities: adapter
|
|
48
|
-
error: null };
|
|
50
|
+
diagnostics: [], needsAction: [], capabilities: effectiveCapabilities(adapter),
|
|
51
|
+
deliveryDiagnostic: null, error: null };
|
|
49
52
|
|
|
50
53
|
try {
|
|
51
54
|
const output = await withTimeout(
|
|
@@ -62,10 +65,12 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
62
65
|
} catch (error) {
|
|
63
66
|
entry.error = error.message;
|
|
64
67
|
}
|
|
68
|
+
entry.capabilities = effectiveCapabilities(adapter,
|
|
69
|
+
{ clientVersion: entry.version, platform });
|
|
65
70
|
|
|
66
71
|
try {
|
|
67
72
|
const detected = await adapter.detect(context);
|
|
68
|
-
entry.diagnostics = detected.diagnostics ?? [];
|
|
73
|
+
entry.diagnostics = [...(detected.diagnostics ?? [])];
|
|
69
74
|
// What a person has to do, as opposed to what is true. Adapters that
|
|
70
75
|
// have nothing to ask for say nothing.
|
|
71
76
|
entry.needsAction = detected.needsAction ?? [];
|
|
@@ -77,6 +82,16 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
77
82
|
} catch (error) {
|
|
78
83
|
entry.error = entry.error ?? error.message;
|
|
79
84
|
}
|
|
85
|
+
if (entry.capabilities?.delivery?.livePush !== true
|
|
86
|
+
&& typeof adapter.deliveryFallback?.diagnostic === "string") {
|
|
87
|
+
const nextTurnDowngraded = adapter.capabilities?.delivery?.nextTurn === true
|
|
88
|
+
&& entry.capabilities?.delivery?.nextTurn !== true;
|
|
89
|
+
entry.deliveryDiagnostic = nextTurnDowngraded
|
|
90
|
+
? `${adapter.displayName} ${entry.version ?? "unknown version"} has no certified `
|
|
91
|
+
+ `next-turn delivery on ${platform}; ${adapter.deliveryFallback.diagnostic}`
|
|
92
|
+
: adapter.deliveryFallback.diagnostic;
|
|
93
|
+
entry.diagnostics.push(entry.deliveryDiagnostic);
|
|
94
|
+
}
|
|
80
95
|
return entry;
|
|
81
96
|
}));
|
|
82
97
|
return entries;
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
export { detectInstallation, spawnProbe } from "./detect.mjs";
|
|
3
3
|
export { planInstallation } from "./plan.mjs";
|
|
4
4
|
export { applyPlan } from "./apply.mjs";
|
|
5
|
-
export { fingerprint, loadOwnership,
|
|
6
|
-
verifyOwned }
|
|
5
|
+
export { finalizeRemoval, fingerprint, loadOwnership, missingArtifactParents, recordInstall,
|
|
6
|
+
removeEmptyOwnedDirectories, removeOwned, removeOwnedArtifacts, treeFingerprint, verifyOwned }
|
|
7
|
+
from "./ownership.mjs";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, readFile, rename, rm, writeFile }
|
|
2
|
+
import { lstat, mkdir, readdir, readFile, rename, rm, rmdir, writeFile }
|
|
3
|
+
from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
@@ -109,7 +110,7 @@ async function saveOwnership({ dataHome, record }) {
|
|
|
109
110
|
* runtime, and leaves the bundle inside the client exactly where it was.
|
|
110
111
|
*/
|
|
111
112
|
export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
|
|
112
|
-
artifacts }) {
|
|
113
|
+
artifacts, createdDirectories = [] }) {
|
|
113
114
|
const stamped = await Promise.all(artifacts.map(async artifact => ({
|
|
114
115
|
path: artifact.path,
|
|
115
116
|
kind: artifact.kind ?? "file",
|
|
@@ -118,14 +119,99 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
118
119
|
sha256: artifact.kind === "merge" ? null : await fingerprintFor(artifact),
|
|
119
120
|
})));
|
|
120
121
|
const record = await loadOwnership({ dataHome });
|
|
122
|
+
const previous = record.installs.find(install => install.adapterId === adapterId);
|
|
123
|
+
const directories = [...new Set([
|
|
124
|
+
...(previous?.createdDirectories ?? []), ...createdDirectories,
|
|
125
|
+
])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length
|
|
126
|
+
|| left.localeCompare(right));
|
|
121
127
|
await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
|
|
122
128
|
installs: [...record.installs.filter(install => install.adapterId !== adapterId),
|
|
123
|
-
{ adapterId, version, accVersion, artifacts: stamped
|
|
129
|
+
{ adapterId, version, accVersion, artifacts: stamped,
|
|
130
|
+
...(directories.length === 0 ? {} : { createdDirectories: directories }) }] } });
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
const installFor = (record, adapterId) =>
|
|
127
134
|
record.installs.find(install => install.adapterId === adapterId) ?? null;
|
|
128
135
|
|
|
136
|
+
const inside = (home, candidate) => {
|
|
137
|
+
const relative = path.relative(home, candidate);
|
|
138
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`)
|
|
139
|
+
&& !path.isAbsolute(relative);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
async function hasSymlinkAncestor(home, candidate) {
|
|
143
|
+
let current = candidate;
|
|
144
|
+
while (inside(home, current)) {
|
|
145
|
+
const stat = await lstat(current).catch(error => {
|
|
146
|
+
if (error.code === "ENOENT") return null;
|
|
147
|
+
throw error;
|
|
148
|
+
});
|
|
149
|
+
if (stat?.isSymbolicLink()) return true;
|
|
150
|
+
current = path.dirname(current);
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Return planned artifact parents that do not yet exist under the client home. */
|
|
156
|
+
export async function missingArtifactParents({ home, artifacts }) {
|
|
157
|
+
if (typeof home !== "string") return [];
|
|
158
|
+
const root = path.resolve(home);
|
|
159
|
+
const missing = new Set();
|
|
160
|
+
for (const artifact of artifacts) {
|
|
161
|
+
let directory = path.dirname(path.resolve(artifact.path));
|
|
162
|
+
while (inside(root, directory)) {
|
|
163
|
+
try {
|
|
164
|
+
await lstat(directory);
|
|
165
|
+
break;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error.code !== "ENOENT") throw error;
|
|
168
|
+
missing.add(directory);
|
|
169
|
+
directory = path.dirname(directory);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [...missing].sort((left, right) =>
|
|
174
|
+
left.split(path.sep).length - right.split(path.sep).length || left.localeCompare(right));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Remove recorded parents deepest-first, but only while each remains an empty directory. */
|
|
178
|
+
export async function removeEmptyOwnedDirectories({ home, directories = [] }) {
|
|
179
|
+
const result = { removed: [], kept: [], missing: [] };
|
|
180
|
+
const root = typeof home === "string" ? path.resolve(home) : null;
|
|
181
|
+
const ordered = [...new Set(directories)].sort((left, right) =>
|
|
182
|
+
right.split(path.sep).length - left.split(path.sep).length || left.localeCompare(right));
|
|
183
|
+
for (const directory of ordered) {
|
|
184
|
+
if (root === null || !inside(root, path.resolve(directory))) {
|
|
185
|
+
result.kept.push(directory);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let stat;
|
|
189
|
+
try {
|
|
190
|
+
stat = await lstat(directory);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error.code === "ENOENT") { result.missing.push(directory); continue; }
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
196
|
+
|| await hasSymlinkAncestor(root, path.dirname(directory))) {
|
|
197
|
+
result.kept.push(directory);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await rmdir(directory);
|
|
202
|
+
result.removed.push(directory);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (["ENOTEMPTY", "EEXIST"].includes(error.code)) {
|
|
205
|
+
result.kept.push(directory);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (error.code === "ENOENT") { result.missing.push(directory); continue; }
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
129
215
|
/** Compare what was written against what is there now. Read-only. */
|
|
130
216
|
export async function verifyOwned({ dataHome, adapterId }) {
|
|
131
217
|
const install = installFor(await loadOwnership({ dataHome }), adapterId);
|
|
@@ -141,17 +227,12 @@ export async function verifyOwned({ dataHome, adapterId }) {
|
|
|
141
227
|
return result;
|
|
142
228
|
}
|
|
143
229
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
* A modified file is kept and reported. A merge artifact is never deleted at
|
|
148
|
-
* all: the user owns that file and ACC owns some entries inside it, which is the
|
|
149
|
-
* adapter's own uninstall to unpick because it knows the format.
|
|
150
|
-
*/
|
|
151
|
-
export async function removeOwned({ dataHome, adapterId }) {
|
|
230
|
+
/** Remove owned artifacts while retaining the record as retry authority. */
|
|
231
|
+
export async function removeOwnedArtifacts({ dataHome, adapterId }) {
|
|
152
232
|
const record = await loadOwnership({ dataHome });
|
|
153
233
|
const install = installFor(record, adapterId);
|
|
154
|
-
const result = { adapterId, removed: [], kept: [], missing: [], delegated: []
|
|
234
|
+
const result = { adapterId, removed: [], kept: [], missing: [], delegated: [],
|
|
235
|
+
createdDirectories: install?.createdDirectories ?? [] };
|
|
155
236
|
if (install === null) return result;
|
|
156
237
|
|
|
157
238
|
for (const artifact of install.artifacts) {
|
|
@@ -163,7 +244,22 @@ export async function removeOwned({ dataHome, adapterId }) {
|
|
|
163
244
|
result.removed.push(artifact.path);
|
|
164
245
|
}
|
|
165
246
|
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Forget one install only after every adapter-owned cleanup step succeeded. */
|
|
251
|
+
export async function finalizeRemoval({ dataHome, adapterId }) {
|
|
252
|
+
const record = await loadOwnership({ dataHome });
|
|
253
|
+
if (installFor(record, adapterId) === null) return false;
|
|
254
|
+
|
|
166
255
|
await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
|
|
167
256
|
installs: record.installs.filter(entry => entry.adapterId !== adapterId) } });
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Remove and finalize for callers that perform no delegated adapter cleanup. */
|
|
261
|
+
export async function removeOwned(options) {
|
|
262
|
+
const result = await removeOwnedArtifacts(options);
|
|
263
|
+
await finalizeRemoval(options);
|
|
168
264
|
return result;
|
|
169
265
|
}
|
|
@@ -8,10 +8,14 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
8
8
|
* it previews is a decoration, and the operator would find out only afterwards.
|
|
9
9
|
*/
|
|
10
10
|
export function planInstallation({ adapters, detected, context, action = "install",
|
|
11
|
-
recorded = [], accVersion = null, allowDowngrade = false, requested = []
|
|
11
|
+
recorded = [], accVersion = null, allowDowngrade = false, requested = [],
|
|
12
|
+
delivery = "off" }) {
|
|
12
13
|
if (!["install", "uninstall"].includes(action)) {
|
|
13
14
|
throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
|
|
14
15
|
}
|
|
16
|
+
if (!["off", "actionable", "all"].includes(delivery)) {
|
|
17
|
+
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${delivery}`, { delivery });
|
|
18
|
+
}
|
|
15
19
|
const byId = new Map(adapters.map(adapter => [adapter.id, adapter]));
|
|
16
20
|
// What ACC recorded writing, by client. For an uninstall this is the
|
|
17
21
|
// authority rather than detection: the record is the only account of what was
|
|
@@ -82,7 +86,16 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
82
86
|
// From the record when the client is gone, because that is what was written
|
|
83
87
|
// and so what will be removed. Asking the adapter instead would describe an
|
|
84
88
|
// install for a machine this one no longer is.
|
|
85
|
-
const
|
|
89
|
+
const liveDeliverySupported = entry.capabilities?.delivery?.livePush === true;
|
|
90
|
+
const effectiveLivePolicy = liveDeliverySupported ? delivery : "off";
|
|
91
|
+
const deliveryDiagnostic = action === "install" && delivery !== "off"
|
|
92
|
+
&& !liveDeliverySupported
|
|
93
|
+
? entry.deliveryDiagnostic ?? adapter.deliveryFallback?.diagnostic
|
|
94
|
+
?? `${adapter.displayName ?? adapter.id} has no certified live delivery for this client; durable fallback remains active`
|
|
95
|
+
: null;
|
|
96
|
+
const installContext = { ...context, requestedLivePolicy: delivery,
|
|
97
|
+
livePolicy: effectiveLivePolicy };
|
|
98
|
+
const artifacts = (record?.artifacts ?? adapter.planInstall(installContext))
|
|
86
99
|
.map(artifact => ({ path: artifact.path, kind: artifact.kind ?? "file" }))
|
|
87
100
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
88
101
|
|
|
@@ -96,12 +109,16 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
96
109
|
// to be inferred from a client version that is null.
|
|
97
110
|
clientPresent: entry.present === true,
|
|
98
111
|
alreadyInstalled: entry.installed === true,
|
|
112
|
+
livePolicy: delivery,
|
|
113
|
+
effectiveLivePolicy,
|
|
114
|
+
...(deliveryDiagnostic === null ? {} : { deliveryDiagnostic }),
|
|
99
115
|
artifacts,
|
|
100
116
|
// Said in the operator's terms, not in paths: which files ACC creates
|
|
101
117
|
// outright and which belong to the user and are only edited.
|
|
102
118
|
summary: [
|
|
103
119
|
...(entry.present ? [] : [`${adapter.displayName ?? adapter.id} is no longer on `
|
|
104
120
|
+ "this machine; removing what ACC recorded writing"]),
|
|
121
|
+
...(deliveryDiagnostic === null ? [] : [deliveryDiagnostic]),
|
|
105
122
|
...artifacts.filter(a => a.kind === "tree")
|
|
106
123
|
.map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
|
|
107
124
|
...artifacts.filter(a => a.kind === "merge")
|