agents-can-communicate 0.1.17 → 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 +76 -138
- package/SECURITY.md +31 -0
- package/bin/acc-hook.mjs +96 -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 +105 -197
- 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 +80 -160
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +15 -5
- 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 +80 -160
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +21 -12
- 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 +80 -160
- 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 +10 -4
- 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 +14 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/hooks/hooks.json +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +152 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +127 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +101 -0
- 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 +80 -160
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +10 -4
- 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 +139 -224
- 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 +2 -1
- 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 +13 -29
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +5 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +109 -109
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +1 -1
- 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 +118 -0
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +14 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +70 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -258
- 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 +156 -60
- 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 +109 -71
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +74 -93
- 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 +86 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +121 -27
- package/package.json +22 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- 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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { effectiveCapabilities } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
|
|
3
|
+
const PLATFORM = `${process.platform}-${process.arch}`;
|
|
4
|
+
const SAFE_ERRORS = new Set(["ambiguous_recipient_sessions", "delivery_disabled",
|
|
5
|
+
"recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected",
|
|
6
|
+
"unsupported_client_version"]);
|
|
7
|
+
const NAMED_LIVE_TRANSPORTS = new Set(["claude-channel", "codex-app-server"]);
|
|
8
|
+
|
|
9
|
+
const adaptersById = adapters => adapters instanceof Map
|
|
10
|
+
? adapters
|
|
11
|
+
: new Map((Array.isArray(adapters) ? adapters : Object.values(adapters ?? {}))
|
|
12
|
+
.map(adapter => [adapter.id, adapter]));
|
|
13
|
+
|
|
14
|
+
const permits = (policy, kind) => policy === "all"
|
|
15
|
+
|| (policy === "actionable" && ["question", "request", "handoff"].includes(kind));
|
|
16
|
+
|
|
17
|
+
const durable = (recipientParticipantId, errorCode) => ({ recipientParticipantId,
|
|
18
|
+
outcome: "queued", transport: "durable", errorCode });
|
|
19
|
+
|
|
20
|
+
const settled = receipt => ({ recipientParticipantId: receipt.recipientParticipantId,
|
|
21
|
+
outcome: receipt.state, transport: "durable" });
|
|
22
|
+
|
|
23
|
+
function safeTransport(value, opaqueEndpointRef) {
|
|
24
|
+
if (NAMED_LIVE_TRANSPORTS.has(value) && value !== opaqueEndpointRef) return value;
|
|
25
|
+
// Both markers are fixed router vocabulary. The alternate prevents even a
|
|
26
|
+
// coincidental endpoint value equal to the primary redaction from escaping.
|
|
27
|
+
return opaqueEndpointRef === "live-adapter" ? "native-live" : "live-adapter";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createDeliveryRouter({ service, adapters, clock }) {
|
|
31
|
+
const registry = adaptersById(adapters);
|
|
32
|
+
|
|
33
|
+
async function recordFailure(binding, message, participantId, transport, safeErrorCode) {
|
|
34
|
+
await service.recordOfferFailed({ messageId: message.messageId,
|
|
35
|
+
recipientParticipantId: participantId, targetSessionId: binding.sessionId,
|
|
36
|
+
targetGeneration: binding.generation,
|
|
37
|
+
transport: safeTransport(transport, binding.opaqueEndpointRef), adapterId: binding.adapterId,
|
|
38
|
+
clientVersion: binding.clientVersion, safeErrorCode }).catch(() => null);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function offerTo(message, participantId, now) {
|
|
42
|
+
const receipt = await service.readReceipt({ messageId: message.messageId,
|
|
43
|
+
recipientParticipantId: participantId });
|
|
44
|
+
if (receipt.state !== "queued") return settled(receipt);
|
|
45
|
+
const liveSessions = await service.listLiveSessions({ participantId, now });
|
|
46
|
+
if (liveSessions.length === 0) return durable(participantId, "recipient_unavailable");
|
|
47
|
+
if (liveSessions.length > 1) {
|
|
48
|
+
return durable(participantId, "ambiguous_recipient_sessions");
|
|
49
|
+
}
|
|
50
|
+
const [target] = liveSessions;
|
|
51
|
+
const bindings = (await service.listDeliveryBindings({ participantId, now }))
|
|
52
|
+
.filter(binding => binding.sessionId === target.sessionId
|
|
53
|
+
&& binding.generation === target.generation);
|
|
54
|
+
if (bindings.length === 0) return durable(participantId, "recipient_unavailable");
|
|
55
|
+
const permitted = bindings.filter(binding => binding.livePolicy !== "off"
|
|
56
|
+
&& permits(binding.livePolicy, message.kind));
|
|
57
|
+
if (permitted.length === 0) return durable(participantId, "delivery_disabled");
|
|
58
|
+
const reachable = permitted.filter(binding => binding.availableModes.includes("livePush"));
|
|
59
|
+
if (reachable.length === 0) return durable(participantId, "recipient_unavailable");
|
|
60
|
+
const certified = reachable.map(binding => ({ binding,
|
|
61
|
+
adapter: registry.get(binding.adapterId) }))
|
|
62
|
+
.filter(({ binding, adapter }) => adapter !== undefined
|
|
63
|
+
&& effectiveCapabilities(adapter,
|
|
64
|
+
{ clientVersion: binding.clientVersion, platform: PLATFORM }).delivery.livePush);
|
|
65
|
+
if (certified.length === 0) {
|
|
66
|
+
return durable(participantId, "unsupported_client_version");
|
|
67
|
+
}
|
|
68
|
+
if (certified.length > 1) {
|
|
69
|
+
return durable(participantId, "ambiguous_recipient_sessions");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { binding, adapter } = certified[0];
|
|
73
|
+
let response;
|
|
74
|
+
try {
|
|
75
|
+
response = await adapter.offerMessage({ binding, message });
|
|
76
|
+
} catch {
|
|
77
|
+
await recordFailure(binding, message, participantId, "live-adapter", "transport_error");
|
|
78
|
+
return durable(participantId, "transport_error");
|
|
79
|
+
}
|
|
80
|
+
const transport = safeTransport(response?.transport, binding.opaqueEndpointRef);
|
|
81
|
+
if (response?.accepted !== true) {
|
|
82
|
+
const code = SAFE_ERRORS.has(response?.safeErrorCode)
|
|
83
|
+
? response.safeErrorCode : "transport_rejected";
|
|
84
|
+
await recordFailure(binding, message, participantId, transport, code);
|
|
85
|
+
return durable(participantId, code);
|
|
86
|
+
}
|
|
87
|
+
if (response.clientVersion !== binding.clientVersion) {
|
|
88
|
+
await recordFailure(binding, message, participantId, transport,
|
|
89
|
+
"unsupported_client_version");
|
|
90
|
+
return durable(participantId, "unsupported_client_version");
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await service.recordOfferSucceeded({ messageId: message.messageId,
|
|
94
|
+
recipientParticipantId: participantId, targetSessionId: binding.sessionId,
|
|
95
|
+
targetGeneration: binding.generation, transport, adapterId: binding.adapterId,
|
|
96
|
+
clientVersion: binding.clientVersion });
|
|
97
|
+
} catch {
|
|
98
|
+
await recordFailure(binding, message, participantId, transport, "transport_error");
|
|
99
|
+
return durable(participantId, "transport_error");
|
|
100
|
+
}
|
|
101
|
+
return { recipientParticipantId: participantId, outcome: "offered", transport };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function offer(message) {
|
|
105
|
+
if (!Array.isArray(message?.toParticipantIds) || message.toParticipantIds.length === 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const now = clock.now();
|
|
109
|
+
const outcomes = [];
|
|
110
|
+
for (const participantId of message.toParticipantIds) {
|
|
111
|
+
outcomes.push(await offerTo(message, participantId, now));
|
|
112
|
+
}
|
|
113
|
+
return outcomes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return Object.freeze({ offer });
|
|
117
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b|$)/;
|
|
4
|
+
|
|
5
|
+
export function parseClientVersion(output) {
|
|
6
|
+
return typeof output === "string" ? VERSION.exec(output)?.[1] ?? null : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Probe the client binary once when its real session starts. */
|
|
10
|
+
export function probeClientVersion(adapter, { timeoutMs = 1_000 } = {}) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
execFile(adapter.client.command, adapter.client.versionArgs ?? ["--version"], {
|
|
13
|
+
timeout: timeoutMs,
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
}, (error, stdout, stderr) => {
|
|
16
|
+
if (error !== null) return resolve(null);
|
|
17
|
+
resolve(parseClientVersion(`${stdout}${stderr}`));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -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,12 +11,40 @@ 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
|
|
|
17
|
+
// Kept cohesive above 300 lines because every handler shares one fail-open
|
|
18
|
+
// hook boundary, binding lifecycle, and client-specific outcome contract.
|
|
19
|
+
// Splitting handlers would duplicate that safety boundary and make delivery or
|
|
20
|
+
// guard failures behave differently by event kind.
|
|
21
|
+
|
|
16
22
|
// A hook runs in front of the user's turn, so it gets a hard ceiling. Better to
|
|
17
23
|
// let a call through than to make someone's session sit waiting on us.
|
|
18
24
|
const DEFAULT_BUDGET_MS = 5_000;
|
|
19
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
|
+
|
|
20
48
|
// Declared by this process on the session it opens, so peers can tell an idle
|
|
21
49
|
// session from a dead one. Only one of the four clients fires a heartbeat event,
|
|
22
50
|
// so the rest refresh here: on every turn, and during a long one whenever the
|
|
@@ -157,14 +185,46 @@ async function openContext({ cwd, dataHome, runtime, env }) {
|
|
|
157
185
|
}
|
|
158
186
|
|
|
159
187
|
const HANDLERS = {
|
|
160
|
-
async sessionStart({ event, context, adapter, adapterId, paths,
|
|
161
|
-
|
|
188
|
+
async sessionStart({ event, context, adapter, adapterId, binding, paths,
|
|
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);
|
|
162
201
|
// Once per session, never per turn. A client that cannot be found yields
|
|
163
202
|
// null, and the session is then judged by age alone - which is exactly the
|
|
164
203
|
// behaviour every session had before this existed.
|
|
165
204
|
const command = adapter.client?.command ?? null;
|
|
166
205
|
const pid = command === null ? null
|
|
167
206
|
: resolveClientPid({ table: await readProcessTable(), from: process.pid, command });
|
|
207
|
+
const metadata = {
|
|
208
|
+
pid,
|
|
209
|
+
enforcement: capabilities.guards?.beforeWrite === true ? "guarded" : "advisory",
|
|
210
|
+
lifecycle: capabilities.lifecycle?.sessionEnd === true ? "managed" : "manual",
|
|
211
|
+
heartbeatCadenceMs: CADENCE_MS,
|
|
212
|
+
checkoutRoot: context.descriptor.git?.worktreeRoot ?? context.descriptor.roots[0],
|
|
213
|
+
branch: context.descriptor.git?.branch ?? null,
|
|
214
|
+
};
|
|
215
|
+
if (binding !== null) {
|
|
216
|
+
const resumed = await context.service.resumeSession({
|
|
217
|
+
sessionId: binding.accSessionId,
|
|
218
|
+
generation: binding.generation,
|
|
219
|
+
...metadata,
|
|
220
|
+
});
|
|
221
|
+
if (resumed !== null) {
|
|
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 };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
168
228
|
const session = await context.service.openSession({
|
|
169
229
|
workspaceId: context.descriptor.id,
|
|
170
230
|
participantId: participantFor(adapterId, event.sessionId, context.env),
|
|
@@ -174,19 +234,15 @@ const HANDLERS = {
|
|
|
174
234
|
// Declared from what this adapter proved, not from the fact that it is an
|
|
175
235
|
// adapter at all. A peer reading the roster can then tell a session whose
|
|
176
236
|
// writes can be stopped from one whose cannot.
|
|
177
|
-
enforcement: capabilities.guards?.beforeWrite === true ? "guarded" : "advisory",
|
|
178
|
-
lifecycle: capabilities.lifecycle?.sessionEnd === true ? "managed" : "manual",
|
|
179
|
-
heartbeatCadenceMs: CADENCE_MS,
|
|
180
237
|
// Which checkout this agent is in. One workspace spans every worktree of
|
|
181
238
|
// a repository, so this is the only thing that distinguishes them.
|
|
182
|
-
|
|
183
|
-
pid,
|
|
184
|
-
branch: context.descriptor.git?.branch ?? null,
|
|
239
|
+
...metadata,
|
|
185
240
|
descriptor: context.descriptor,
|
|
186
241
|
});
|
|
187
242
|
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
188
|
-
accSessionId: session.sessionId, generation: session.generation });
|
|
189
|
-
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 };
|
|
190
246
|
},
|
|
191
247
|
|
|
192
248
|
async heartbeat({ binding, context }) {
|
|
@@ -204,7 +260,7 @@ const HANDLERS = {
|
|
|
204
260
|
return {};
|
|
205
261
|
},
|
|
206
262
|
|
|
207
|
-
async beforeTurn({ binding, context, adapter }) {
|
|
263
|
+
async beforeTurn({ binding, context, adapter, adapterId }) {
|
|
208
264
|
if (binding === null) return {};
|
|
209
265
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
210
266
|
// this runs in front of somebody's prompt.
|
|
@@ -213,36 +269,21 @@ const HANDLERS = {
|
|
|
213
269
|
const sync = await context.service.sync({ sessionId: binding.accSessionId,
|
|
214
270
|
cursor: null, scope: "delta" });
|
|
215
271
|
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
// plainly that the responsibility has moved to the session itself.
|
|
221
|
-
const status = await context.service.collectStatus({
|
|
222
|
-
workspaceId: context.descriptor.id });
|
|
223
|
-
const mine = status.participants
|
|
272
|
+
// Sync already carries the roster. Calling collectStatus here used to read
|
|
273
|
+
// the entire materialised store a second time on every prompt merely to
|
|
274
|
+
// rediscover this session's participant id.
|
|
275
|
+
const mine = sync.roster
|
|
224
276
|
.find(participant => participant.sessionId === binding.accSessionId);
|
|
225
|
-
// Two independent facts, and both are needed. `enforceable` is whether ACC
|
|
226
|
-
// could stop *this* session at all; `enforcement` is what the claim's owner
|
|
227
|
-
// asked for. A guarded session facing an advisory claim is not blocked from
|
|
228
|
-
// anything, so reporting either one alone mislabels the other case.
|
|
229
|
-
const enforceable = mine?.enforcement === "guarded";
|
|
230
|
-
const claims = status.claims
|
|
231
|
-
.filter(claim => claim.ownerSessionId !== binding.accSessionId)
|
|
232
|
-
.map(claim => ({ resource: claim.resource, enforcement: claim.enforcement,
|
|
233
|
-
enforceable,
|
|
234
|
-
ownerParticipantId: status.participants
|
|
235
|
-
.find(p => p.sessionId === claim.ownerSessionId)?.participantId
|
|
236
|
-
?? claim.ownerSessionId }));
|
|
237
277
|
|
|
238
278
|
// What peers have said to this participant and no model has been shown yet.
|
|
239
|
-
// Without this the projector's peer block never
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
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({
|
|
243
283
|
workspaceId: context.descriptor.id,
|
|
244
284
|
participantId: mine?.participantId,
|
|
245
285
|
exceptSessionId: binding.accSessionId });
|
|
286
|
+
const messages = delivery.queuedMessages;
|
|
246
287
|
|
|
247
288
|
// Solo costs nothing: nothing to say means nothing printed, not a banner
|
|
248
289
|
// announcing that nobody else is here. But something already said to you is
|
|
@@ -254,32 +295,62 @@ const HANDLERS = {
|
|
|
254
295
|
// The ceiling a team agreed on in `acc.workspace.json`, or the default when
|
|
255
296
|
// there is no config. Validated by the protocol and, until now, never read:
|
|
256
297
|
// the projector was always called with its own default.
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
304
|
+
currentParticipantId: mine?.participantId };
|
|
305
|
+
const projectionOptions = {
|
|
306
|
+
budgetBytes: context.descriptor.policy?.contextBudgetBytes };
|
|
307
|
+
// Delivery is state, not text parsing. Peer-controlled bodies can imitate
|
|
308
|
+
// another message's visible header, so only projector metadata proves
|
|
309
|
+
// which complete groups survived the byte budget. A custom adapter without
|
|
310
|
+
// metadata may still inject text, but cannot advance a receipt from it.
|
|
311
|
+
const projection = !hasStructuredRenderer
|
|
312
|
+
? { text: await adapter.renderContext?.(projectionInput, projectionOptions) ?? "",
|
|
313
|
+
offeredMessageIds: [], includedAttentionIds: [] }
|
|
314
|
+
: await adapter.renderContextResult(projectionInput, projectionOptions);
|
|
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;
|
|
325
|
+
const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
|
|
326
|
+
const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
|
|
327
|
+
const projected = degradation === null ? projection.text
|
|
328
|
+
: fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
|
|
329
|
+
if (projected === "") {
|
|
330
|
+
return degradation === null ? { stdout: "" } : { stdout: "", stderr: degradation };
|
|
272
331
|
}
|
|
332
|
+
|
|
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 }));
|
|
273
343
|
// Same again: Kimi Code shows the model a hook's raw stdout, while Gemini
|
|
274
344
|
// and Claude Code want an envelope and drop a bare string.
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
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.
|
|
278
348
|
const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
|
|
279
|
-
|
|
349
|
+
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
350
|
+
if (degradation === null) return { ...outcome, offerInputs: writableOffers };
|
|
280
351
|
return { ...outcome,
|
|
281
|
-
stderr: [outcome.stderr,
|
|
282
|
-
|
|
352
|
+
stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
|
|
353
|
+
offerInputs: writableOffers };
|
|
283
354
|
},
|
|
284
355
|
|
|
285
356
|
async beforeTool({ binding, context, event, adapter }) {
|
|
@@ -350,8 +421,12 @@ const HANDLERS = {
|
|
|
350
421
|
*/
|
|
351
422
|
export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
352
423
|
runtime = defaultRuntime(), budgetMs = DEFAULT_BUDGET_MS,
|
|
353
|
-
readProcessTable = defaultReadProcessTable
|
|
354
|
-
|
|
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 () => {} };
|
|
355
430
|
let timer = null;
|
|
356
431
|
try {
|
|
357
432
|
const adapter = adapters?.[adapterId];
|
|
@@ -366,7 +441,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
366
441
|
const work = handler === undefined
|
|
367
442
|
? Promise.resolve({})
|
|
368
443
|
: handler({ event, context, adapter, adapterId, binding, paths: context.paths,
|
|
369
|
-
readProcessTable });
|
|
444
|
+
readProcessTable, probeClientVersion, platform, deadline });
|
|
370
445
|
|
|
371
446
|
// The loser of a race is not cancelled, so the timer is cleared explicitly:
|
|
372
447
|
// an outstanding one keeps the process alive long past its answer.
|
|
@@ -375,6 +450,24 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
375
450
|
});
|
|
376
451
|
Object.assign(result, await Promise.race([work, budget]));
|
|
377
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
|
+
|
|
378
471
|
const status = await context.service.collectStatus({
|
|
379
472
|
workspaceId: context.descriptor.id });
|
|
380
473
|
result.sessions = status.participants.filter(p => p.presence !== "offline");
|
|
@@ -384,6 +477,9 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
384
477
|
result.reason = error.message;
|
|
385
478
|
result.decision = "allow";
|
|
386
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 () => {};
|
|
387
483
|
} finally {
|
|
388
484
|
if (timer !== null) clearTimeout(timer);
|
|
389
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";
|