agents-can-communicate 0.5.3 → 0.5.5

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.
Files changed (46) hide show
  1. package/README.md +8 -5
  2. package/docs/ARCHITECTURE.md +6 -1
  3. package/docs/CLI.md +7 -4
  4. package/docs/CONFIGURATION.md +7 -3
  5. package/docs/GETTING_STARTED.md +12 -3
  6. package/docs/TROUBLESHOOTING.md +25 -9
  7. package/docs/UPGRADING.md +39 -10
  8. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  9. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  10. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +5 -2
  11. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +2 -1
  12. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +1 -1
  13. package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +37 -12
  14. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +1 -1
  15. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +2 -1
  16. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +135 -0
  17. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  18. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  19. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  20. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  21. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  23. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +10 -5
  24. package/node_modules/@agents-can-communicate/cli/src/help.mjs +3 -0
  25. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +15 -71
  26. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +152 -0
  27. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation-files.mjs +25 -0
  28. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +1 -22
  29. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +1 -1
  30. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +2 -2
  31. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +6 -1
  32. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +23 -1
  33. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +22 -5
  34. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  36. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  38. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +6 -2
  39. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +11 -0
  40. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -1
  41. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -2
  42. package/node_modules/@agents-can-communicate/installer/src/service-setup.mjs +30 -0
  43. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  44. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  45. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  46. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -6,12 +6,12 @@ import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnersh
6
6
  from "@agents-can-communicate/installer";
7
7
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
8
8
 
9
- import { describeNative, nativeRemediation, nativeState, updateNativeRuntime } from "./native-delivery-status.mjs";
9
+ import { describeDeliveryDecision, describeNative, nativeRemediation, nativeState,
10
+ updateNativeRuntime } from "./native-delivery-status.mjs";
10
11
  export { describeNative } from "./native-delivery-status.mjs";
11
-
12
12
  import { nativeSessionLines, updateNativeSessions } from "./native-session-diagnostics.mjs";
13
-
14
13
  import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs";
14
+ import { decisionOf } from "./install-delivery-consent.mjs";
15
15
  import { describePresence } from "./main.mjs";
16
16
  import { platformPaths } from "./platform-paths.mjs";
17
17
  import { noticeUpdate } from "./update-check.mjs";
@@ -173,7 +173,9 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
173
173
  // is the acc that copied the skills and manifests into it. They diverge after
174
174
  // an npm upgrade with no `acc install`, and reporting both is what makes the
175
175
  // divergence legible rather than hidden behind a single reassuring number.
176
- return { ...entry, stale, wired, bundleVersion, owned: { modified: owned.modified,
176
+ return { ...entry, stale, wired, bundleVersion,
177
+ deliveryDecision: installed === undefined ? null : decisionOf(installed),
178
+ owned: { modified: owned.modified,
177
179
  missing: owned.missing, intact: owned.intact.length },
178
180
  nativeDelivery: nativeState(entry.nativeDelivery, livePolicyOf(installed), {
179
181
  contract: adapters.find(adapter => adapter.id === entry.adapterId)?.nativeDelivery,
@@ -241,6 +243,7 @@ export async function runDoctor({ options, context, runtime }) {
241
243
  registry: ALL_ADAPTERS() });
242
244
  for (const adapter of adapters) {
243
245
  adapter.remediation.push(...nativeRemediation(adapter));
246
+ adapter.remediation = [...new Set(adapter.remediation)];
244
247
  }
245
248
 
246
249
  const data = {
@@ -276,9 +279,11 @@ export async function runDoctor({ options, context, runtime }) {
276
279
  ...adapters.filter(adapter => adapter.present)
277
280
  .map(adapter => ` ${adapter.displayName} live delivery: `
278
281
  + `${describeNative(adapter.nativeDelivery, { clientVersion: adapter.version })}; `
282
+ + `${adapter.nativeDelivery.policy === "off" && adapter.deliveryDecision !== null
283
+ ? `decision: ${describeDeliveryDecision(adapter.deliveryDecision)}; ` : ""}`
279
284
  + `fallback: ${describeDeliveryFallback(adapter)}`),
280
285
  ...nativeSessionLines(adapters),
281
- ...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery)
286
+ ...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery?.state === "configured")
282
287
  .map(adapter => ` ${adapter.displayName} ${adapter.outgoingDelivery.diagnostic}`),
283
288
  ...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
284
289
  + (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(update.notice ? [` ${update.notice}`] : [])]),
@@ -76,6 +76,9 @@ const CHOICES = Object.freeze({
76
76
  });
77
77
 
78
78
  const NOTES = Object.freeze({
79
+ install: ["Without --delivery, one default-No question covers all selected clients that need a decision.",
80
+ "--delivery actionable|all gives explicit complete-setup consent; --delivery off disables incoming automatic requests.",
81
+ "A dry run does not prompt, write configuration, or start a service. Service readiness does not establish a session binding."],
79
82
  attach: ["Native hooks already manage their own session. Attach only for a manual CLI session.",
80
83
  "Keep the returned session and generation together for subsequent calls.",
81
84
  "--cadence is a positive interval in milliseconds."],
@@ -6,13 +6,16 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
6
6
  import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
7
7
  import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
8
8
  import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
9
- import { LIVE_POLICIES, applyPlan, detectInstallation, livePolicyOf,
9
+ import { applyPlan, detectInstallation,
10
10
  loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
11
11
  from "@agents-can-communicate/installer";
12
12
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
13
13
 
14
14
  import { platformPaths } from "./platform-paths.mjs";
15
15
  import { installManaged, uninstallManaged } from "./managed-runtime/install.mjs";
16
+ import { decideDelivery } from "./install-delivery-consent.mjs";
17
+
18
+ export { decideDelivery } from "./install-delivery-consent.mjs";
16
19
 
17
20
  // Kept cohesive above 300 lines because this is the install command boundary:
18
21
  // detection, consent, planning, application, and reporting share one context
@@ -180,10 +183,11 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
180
183
  * what a malformed `~/.claude/settings.json` produced: the adapter refused,
181
184
  * correctly, and the script that ran the installer was told it had worked.
182
185
  */
183
- export function failureOf({ action, acted, failed = [] }) {
186
+ export function failureOf({ action, acted, failed = [], operations = [], skipped = [], home }) {
184
187
  if (failed.length === 0) return null;
185
188
  return new AccError(EXIT.DATA,
186
- describeOutcome({ action, acted, failed }), { failed });
189
+ [describeOutcome({ action, acted, failed, operations, skipped, home }),
190
+ ...reloadAdvice(operations)].join("\n"), { failed });
187
191
  }
188
192
 
189
193
  /**
@@ -202,70 +206,6 @@ export function actedOn(result) {
202
206
  + (operation.changes?.length ?? 0) > 0)).length;
203
207
  }
204
208
 
205
- const isInteractive = runtime => typeof runtime.isInteractive === "function"
206
- && runtime.isInteractive() === true;
207
-
208
- // One default-No question per supported client. Disclose token use and any
209
- // repeated channel prompt; declining only promises a verified fallback.
210
- function questionFor(entry) {
211
- const bootstrap = entry.nativeDelivery.activationPlan?.mechanisms
212
- .find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
213
- const warns = (bootstrap?.prefixArgs ?? []).some(argument => argument.startsWith("--"));
214
- return [
215
- `Let ${entry.displayName} answer peer requests while idle? (experimental)`,
216
- " Yes: automatic turns can spend tokens without waiting for you.",
217
- ...(entry.outgoingDelivery?.setup ? [` ${entry.outgoingDelivery.setup}`] : []),
218
- ...(warns ? [" Allow development channels when prompted; check the client's startup notice."] : []),
219
- ...(entry.nativeDelivery.state !== "eligible"
220
- ? [" Save consent now; delivery waits for an available client service."] : []),
221
- entry.capabilities?.delivery?.nextTurn === true
222
- ? " No: use next-turn hooks (when enabled) or acc inbox."
223
- : " No: read messages with acc inbox; automatic next-turn delivery is unavailable.",
224
- ].join("\n");
225
- }
226
-
227
- /**
228
- * Which live policy each selected client gets.
229
- *
230
- * An explicit --delivery applies uniformly and never prompts. Otherwise a
231
- * recorded opt-in is kept as it was consented to, a fresh or never-activated
232
- * client is off unless a person answers yes here, and a dry run or a
233
- * non-interactive stdin/stdout makes no interactive choice at all.
234
- */
235
- export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
236
- const explicit = options.delivery;
237
- if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
238
- throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
239
- }
240
- const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
241
- const deliveryByAdapter = {};
242
- const asked = [];
243
- let withheld = 0;
244
- for (const entry of detected) {
245
- const eligible = entry.nativeDelivery?.state === "eligible"
246
- || entry.nativeDelivery?.consentAvailable === true;
247
- const previous = livePolicyOf(recordedById.get(entry.adapterId));
248
- if (explicit !== undefined) {
249
- deliveryByAdapter[entry.adapterId] = explicit;
250
- } else if (previous !== "off") {
251
- deliveryByAdapter[entry.adapterId] = previous;
252
- } else if (!eligible || dryRun || !isInteractive(runtime)) {
253
- deliveryByAdapter[entry.adapterId] = "off";
254
- if (eligible) withheld += 1;
255
- } else {
256
- const yes = await runtime.confirm(questionFor(entry),
257
- { input: runtime.input, output: runtime.output }) === true;
258
- deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
259
- asked.push(entry.adapterId);
260
- }
261
- }
262
- const notes = explicit === undefined && dryRun && withheld > 0
263
- ? ["interactive choices were not made: this preview keeps native delivery off for "
264
- + `${withheld} eligible client(s) without a recorded opt-in`]
265
- : [];
266
- return { deliveryByAdapter, asked, notes };
267
- }
268
-
269
209
  // Said once, after the first PATH block is written: a running shell and a
270
210
  // running client know nothing about it.
271
211
  function reloadAdvice(operations) {
@@ -315,7 +255,9 @@ export async function runInstallCommand({ options, runtime, action = "install" }
315
255
  : { deliveryByAdapter: {}, asked: [], notes: [] };
316
256
  const plan = planInstallation({ adapters, detected, context, action, recorded,
317
257
  accVersion, allowDowngrade: options.downgrade === true, requested,
318
- deliveryByAdapter: decided.deliveryByAdapter });
258
+ deliveryByAdapter: decided.deliveryByAdapter,
259
+ deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
260
+ allowServiceSetup: action === "install" });
319
261
  const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
320
262
  dataHome, dryRun, accVersion, activation: { bootstrap: paths.bootstrap } });
321
263
  const result = action === "install" && !dryRun && runtime.packageRoot
@@ -328,16 +270,18 @@ export async function runInstallCommand({ options, runtime, action = "install" }
328
270
 
329
271
  const acted = actedOn(result);
330
272
  if (dryRun) {
331
- return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter },
273
+ return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
274
+ deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter },
332
275
  text: [`would ${action}:`, ...plan.operations.flatMap(operation => operation.summary),
333
276
  ...plan.skipped.map(entry => `skip ${entry.adapterId}: ${entry.reason}`),
334
277
  ...decided.notes].join("\n") };
335
278
  }
336
279
 
337
280
  return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
338
- asked: decided.asked },
281
+ deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter, asked: decided.asked },
339
282
  text: [describeOutcome({ action, acted, failed: result.failed,
340
283
  skipped: plan.skipped, operations: result.operations, home }),
341
284
  ...reloadAdvice(result.operations)].join("\n"),
342
- error: failureOf({ action, acted, failed: result.failed }) };
285
+ error: failureOf({ action, acted, failed: result.failed, operations: result.operations,
286
+ skipped: plan.skipped, home }) };
343
287
  }
@@ -0,0 +1,152 @@
1
+ import { LIVE_POLICIES, livePolicyOf } from "@agents-can-communicate/installer";
2
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
3
+
4
+ const DECISION_SOURCES = new Set([
5
+ "interactive-accepted",
6
+ "interactive-declined",
7
+ "explicit-option",
8
+ "noninteractive-default",
9
+ "unsupported-default",
10
+ "legacy-unknown",
11
+ ]);
12
+ const DELIBERATE_SOURCES = new Set([
13
+ "interactive-accepted", "interactive-declined", "explicit-option",
14
+ ]);
15
+
16
+ const legacyDecision = () => ({ source: "legacy-unknown", completeSetup: false });
17
+
18
+ function validDecision(record) {
19
+ const decision = record?.deliveryDecision;
20
+ if (!(decision !== null && typeof decision === "object"
21
+ && DECISION_SOURCES.has(decision.source)
22
+ && typeof decision.completeSetup === "boolean")) return false;
23
+
24
+ const enabled = livePolicyOf(record) !== "off";
25
+ if (decision.source === "interactive-accepted") return decision.completeSetup && enabled;
26
+ if (decision.source === "interactive-declined") return !decision.completeSetup && !enabled;
27
+ if (decision.source === "explicit-option") return decision.completeSetup === enabled;
28
+ if (["noninteractive-default", "unsupported-default"].includes(decision.source)) {
29
+ return !decision.completeSetup && !enabled;
30
+ }
31
+ return decision.completeSetup === false;
32
+ }
33
+
34
+ export function decisionOf(record) {
35
+ return validDecision(record)
36
+ ? { source: record.deliveryDecision.source,
37
+ completeSetup: record.deliveryDecision.completeSetup }
38
+ : legacyDecision();
39
+ }
40
+
41
+ const isEligible = entry => entry.nativeDelivery?.state === "eligible"
42
+ || entry.nativeDelivery?.consentAvailable === true;
43
+
44
+ const needsExpandedSetup = entry => ["needed", "blocked"]
45
+ .includes(entry.nativeServiceSetup?.state);
46
+
47
+ const isInteractive = runtime => typeof runtime.isInteractive === "function"
48
+ && runtime.isInteractive() === true;
49
+
50
+ function questionFor(entries) {
51
+ const names = entries.map(entry => entry.displayName ?? entry.adapterId).join(", ");
52
+ const setup = [...new Set(entries.map(entry => entry.outgoingDelivery?.setup)
53
+ .filter(value => typeof value === "string"))];
54
+ const channels = entries.some(entry => (entry.nativeDelivery?.activationPlan?.mechanisms ?? [])
55
+ .some(mechanism => mechanism.kind === "shell-bootstrap"
56
+ && (mechanism.prefixArgs ?? []).some(argument => argument.startsWith("--"))));
57
+ return [
58
+ `Complete automatic peer-request setup for ${names}? (experimental)`,
59
+ " Yes: automatic turns can spend tokens without waiting for you.",
60
+ " Configure the required local permission grants.",
61
+ ...(entries.some(entry => entry.adapterId === "codex")
62
+ ? [" Start a missing supported Codex service."] : []),
63
+ ...setup.map(note => ` ${note}`),
64
+ ...(channels ? [" Allow Claude Code development Channels when prompted at startup."] : []),
65
+ " No: keep current delivery policies and use available next-turn hooks or acc inbox.",
66
+ ].join("\n");
67
+ }
68
+
69
+ /** Decide delivery once for all clients whose complete setup has no recorded answer. */
70
+ export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
71
+ const explicit = options.delivery;
72
+ if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
73
+ throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
74
+ }
75
+
76
+ const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
77
+ const deliveryByAdapter = {};
78
+ const deliveryDecisionByAdapter = {};
79
+ const candidates = [];
80
+ let withheld = 0;
81
+
82
+ for (const entry of detected) {
83
+ const record = recordedById.get(entry.adapterId);
84
+ const previous = livePolicyOf(record);
85
+ const eligible = isEligible(entry);
86
+
87
+ if (explicit !== undefined) {
88
+ deliveryByAdapter[entry.adapterId] = explicit;
89
+ deliveryDecisionByAdapter[entry.adapterId] = {
90
+ source: "explicit-option", completeSetup: explicit !== "off",
91
+ };
92
+ continue;
93
+ }
94
+
95
+ deliveryByAdapter[entry.adapterId] = previous;
96
+ if (record !== undefined) {
97
+ deliveryDecisionByAdapter[entry.adapterId] = decisionOf(record);
98
+ }
99
+
100
+ if (!eligible) {
101
+ deliveryDecisionByAdapter[entry.adapterId] ??= {
102
+ source: "unsupported-default", completeSetup: false,
103
+ };
104
+ continue;
105
+ }
106
+
107
+ const alreadyDecided = validDecision(record)
108
+ && DELIBERATE_SOURCES.has(record.deliveryDecision.source);
109
+ const expandsLegacyOptIn = previous !== "off" && needsExpandedSetup(entry)
110
+ && !decisionOf(record).completeSetup;
111
+ const freshDecision = previous === "off" && !alreadyDecided;
112
+ if (!freshDecision && !expandsLegacyOptIn) {
113
+ deliveryDecisionByAdapter[entry.adapterId] ??= legacyDecision();
114
+ continue;
115
+ }
116
+
117
+ if (dryRun || !isInteractive(runtime)) {
118
+ if (record === undefined) {
119
+ deliveryDecisionByAdapter[entry.adapterId] = {
120
+ source: "noninteractive-default", completeSetup: false,
121
+ };
122
+ }
123
+ if (dryRun && freshDecision) withheld += 1;
124
+ continue;
125
+ }
126
+ candidates.push(entry);
127
+ }
128
+
129
+ const asked = candidates.map(entry => entry.adapterId);
130
+ if (candidates.length > 0) {
131
+ const yes = await runtime.confirm(questionFor(candidates),
132
+ { input: runtime.input, output: runtime.output }) === true;
133
+ for (const entry of candidates) {
134
+ const record = recordedById.get(entry.adapterId);
135
+ const previous = livePolicyOf(record);
136
+ deliveryByAdapter[entry.adapterId] = yes
137
+ ? (previous === "off" ? "actionable" : previous)
138
+ : previous;
139
+ deliveryDecisionByAdapter[entry.adapterId] = yes
140
+ ? { source: "interactive-accepted", completeSetup: true }
141
+ : previous === "off"
142
+ ? { source: "interactive-declined", completeSetup: false }
143
+ : decisionOf(record);
144
+ }
145
+ }
146
+
147
+ const notes = explicit === undefined && dryRun && withheld > 0
148
+ ? ["interactive choices were not made: this preview keeps native delivery off for "
149
+ + `${withheld} eligible client(s) without a recorded opt-in`]
150
+ : [];
151
+ return { deliveryByAdapter, deliveryDecisionByAdapter, asked, notes };
152
+ }
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ export async function collectFile(source, relative, collected) {
6
+ const stat = await lstat(source);
7
+ if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
8
+ if (stat.isDirectory()) {
9
+ for (const entry of (await readdir(source)).sort()) {
10
+ await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
11
+ }
12
+ } else if (stat.isFile()) {
13
+ collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
14
+ } else throw new Error(`unsupported runtime file: ${relative}`);
15
+ }
16
+
17
+ /** This format also names existing generations; keep it stable across upgrades. */
18
+ export function fingerprint(files) {
19
+ const hash = createHash("sha256");
20
+ for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
21
+ hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
22
+ hash.update(file.bytes);
23
+ }
24
+ return hash.digest("hex");
25
+ }
@@ -1,8 +1,8 @@
1
- import { createHash } from "node:crypto";
2
1
  import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm }
3
2
  from "node:fs/promises";
4
3
  import path from "node:path";
5
4
 
5
+ import { collectFile, fingerprint } from "./generation-files.mjs";
6
6
  import { attachStagingTemp, holdStagedGeneration, releaseStagingHold } from "./staging.mjs";
7
7
 
8
8
  const NAME = "agents-can-communicate";
@@ -37,18 +37,6 @@ function declaredPath(value) {
37
37
  return value.replace(/\/+$/, "");
38
38
  }
39
39
 
40
- async function collectFile(source, relative, collected) {
41
- const stat = await lstat(source);
42
- if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
43
- if (stat.isDirectory()) {
44
- for (const entry of (await readdir(source)).sort()) {
45
- await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
46
- }
47
- } else if (stat.isFile()) {
48
- collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
49
- } else throw new Error(`unsupported runtime file: ${relative}`);
50
- }
51
-
52
40
  async function checkDeclaredComponents(source, relative) {
53
41
  let current = source;
54
42
  for (const component of relative.split("/")) {
@@ -84,15 +72,6 @@ async function collectPackage(source, prefix, collected, expectedName, version)
84
72
  return manifest;
85
73
  }
86
74
 
87
- function fingerprint(files) {
88
- const hash = createHash("sha256");
89
- for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
90
- hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
91
- hash.update(file.bytes);
92
- }
93
- return hash.digest("hex");
94
- }
95
-
96
75
  async function existingMatches(target, files) {
97
76
  try {
98
77
  const actual = new Map();
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { ENTRY_KINDS } from "./entry.mjs";
5
5
  import { managedDirectory, syncDirectory } from "./state.mjs";
6
6
 
7
- const MODULES = ["entry.mjs", "state.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
7
+ const MODULES = ["entry.mjs", "state.mjs", "generation-files.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
8
8
  async function durableFile(file, bytes, mode = 0o600) {
9
9
  const handle = await open(file, "wx", mode);
10
10
  try { await handle.writeFile(bytes); await handle.sync(); }
@@ -49,11 +49,11 @@ export async function listRuntimeHolds(root, { pidIsAlive = defaultPidIsAlive }
49
49
  || typeof lease.token !== "string" || name !== `${lease.token}.json`
50
50
  || typeof lease.kind !== "string" || !lease.kind
51
51
  || !Number.isFinite(Date.parse(lease.createdAt))) throw new Error("invalid runtime lease");
52
- await validateRuntime(root, lease.runtime);
52
+ const runtime = await validateRuntime(root, lease.runtime);
53
53
  if (await confirmedDead(lease.pid, pidIsAlive)) {
54
54
  await rm(file, { force: true });
55
55
  removed = true;
56
- } else holds.push(lease);
56
+ } else holds.push({ ...lease, runtime });
57
57
  }
58
58
  if (removed) await syncDirectory(directory);
59
59
  return holds;
@@ -18,11 +18,16 @@ export async function prepareRefresh({ control, root, env = process.env, callerP
18
18
  const detected = await detectInstallation({ adapters, context, probeTimeoutMs: probeTimeout(env) });
19
19
  const deliveryByAdapter = Object.fromEntries(adapters.map(adapter => [adapter.id,
20
20
  livePolicyOf(recorded.find(record => record.adapterId === adapter.id))]));
21
+ const deliveryDecisionByAdapter = Object.fromEntries(adapters.flatMap(adapter => {
22
+ const decision = recorded.find(record => record.adapterId === adapter.id)?.deliveryDecision;
23
+ return decision === undefined ? [] : [[adapter.id, decision]];
24
+ }));
21
25
  // Refresh configuration even when the client's service is temporarily absent.
22
26
  // The installer preserves consent and existing activation; readiness stays a
23
27
  // separate, current probe result rather than an update prerequisite.
24
28
  const plan = planInstallation({ adapters, detected, context, recorded,
25
- accVersion: control.pending.version, requested: control.targets, deliveryByAdapter });
29
+ accVersion: control.pending.version, requested: control.targets, deliveryByAdapter,
30
+ deliveryDecisionByAdapter });
26
31
  if (plan.skipped.length || plan.operations.length !== wanted.size) {
27
32
  throw new Error("candidate did not plan every installed integration");
28
33
  }
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { collectFile, fingerprint } from "./generation-files.mjs";
5
6
 
6
7
  /** Resolve existing ancestors without creating an uninitialized manager. */
7
8
  export async function canonicalManagerRoot(root) {
@@ -74,6 +75,25 @@ export async function writeManagedJson(file, value) {
74
75
  const nonempty = value => typeof value === "string" && value.length > 0;
75
76
  const nullableString = value => value === null || nonempty(value);
76
77
 
78
+ /** Older updaters dropped the contract while publishing a newer generation.
79
+ * Recover only from that exact, unchanged generation, never from the active
80
+ * version or a loose manifest. Explicit lease declarations remain historical
81
+ * facts. Missing or damaged files leave an unknown hold without failing hooks.
82
+ */
83
+ async function recoverStoreVersion(generations, runtime) {
84
+ if (path.dirname(runtime.root) !== generations
85
+ || !/^\d+\.\d+\.\d+-[a-f0-9]{24}$/.test(path.basename(runtime.root))) return null;
86
+ try {
87
+ const files = new Map();
88
+ await collectFile(runtime.root, "", files);
89
+ if (path.basename(runtime.root) !== `${runtime.version}-${fingerprint(files).slice(0, 24)}`) return null;
90
+ const manifest = JSON.parse(files.get("package.json")?.bytes.toString("utf8"));
91
+ return manifest.name === "agents-can-communicate" && manifest.version === runtime.version
92
+ && Number.isSafeInteger(manifest.accStoreVersion) && manifest.accStoreVersion > 0
93
+ ? manifest.accStoreVersion : null;
94
+ } catch { return null; }
95
+ }
96
+
77
97
  export async function validateRuntime(root, runtime) {
78
98
  if (!runtime || !nonempty(runtime.version) || typeof runtime.root !== "string"
79
99
  || !path.isAbsolute(runtime.root)) throw new Error("invalid control generation");
@@ -95,7 +115,9 @@ export async function validateRuntime(root, runtime) {
95
115
  if (await managedDirectory(generations)) {
96
116
  if (await realpath(generations) !== generations) throw new Error("invalid control generations directory");
97
117
  }
98
- return { version: runtime.version, root: resolved, storeVersion: runtime.storeVersion ?? null };
118
+ const normalized = { version: runtime.version, root: resolved, storeVersion: runtime.storeVersion ?? null };
119
+ normalized.storeVersion ??= await recoverStoreVersion(generations, normalized);
120
+ return normalized;
99
121
  }
100
122
 
101
123
  async function validateControl(root, value) {
@@ -1,5 +1,13 @@
1
1
  import { describeNativeReason } from "@agents-can-communicate/installer";
2
2
 
3
+ export function describeDeliveryDecision(decision) {
4
+ if (decision?.source === "interactive-declined") return "declined during interactive setup";
5
+ if (decision?.source === "explicit-option") return "disabled by explicit option";
6
+ if (decision?.source === "noninteractive-default") return "defaulted off in a noninteractive run";
7
+ if (decision?.source === "unsupported-default") return "defaulted off because delivery was unsupported";
8
+ return "origin unknown (legacy install)";
9
+ }
10
+
3
11
  export function describeNative(native, { clientVersion } = {}) {
4
12
  const differs = native.runtime === "active" && native.sessionPolicy
5
13
  && native.sessionPolicy !== native.policy;
@@ -20,24 +28,33 @@ export function nativeRemediation(entry) {
20
28
  const native = entry.nativeDelivery;
21
29
  if (!(entry.present || entry.installed) || native.eligibility === "unsupported") return [];
22
30
  const steps = [];
31
+ const service = entry.nativeServiceSetup;
23
32
  if (!native.configured && native.runtime !== "active") {
24
33
  steps.push(`acc install --adapter ${entry.adapterId} --delivery actionable`
25
34
  + " # opt in to automatic peer requests; may spend tokens");
26
35
  } else if (native.configured && native.activation === "missing") {
27
36
  steps.push(`acc install --adapter ${entry.adapterId}`
28
37
  + " # complete the missing native launch setup from a supported shell");
29
- } else if (native.configured && native.runtime !== "active") {
38
+ } else if (native.configured && native.runtime !== "active"
39
+ && !["needed", "blocked"].includes(service?.state)) {
30
40
  steps.push(`${entry.displayName}: no verified live channel in this workspace; `
31
41
  + "open a new terminal, start a new client session and check its integration/channel prompts; "
32
42
  + "then run acc doctor here");
33
43
  }
34
44
  if (native.reasonCode === "native_endpoint_unavailable") {
35
- steps.push(entry.nativeSetup ?? `${entry.displayName}: check the client's local delivery service setup; `
36
- + "ACC does not start or restart that service");
37
- } else if (native.reasonCode === "native_session_unavailable") {
45
+ if (service?.state === "blocked") steps.push(service.diagnostic);
46
+ else if (service?.state === "needed" && native.configured) {
47
+ steps.push(entry.deliveryDecision?.completeSetup === true
48
+ ? `acc install --adapter ${entry.adapterId} # prepare the missing supported local service`
49
+ : `acc install --adapter ${entry.adapterId} --delivery actionable`
50
+ + " # approve complete automatic peer-request setup");
51
+ } else if (service?.state !== "needed" && service?.state !== "ready") {
52
+ steps.push(entry.nativeSetup ?? `${entry.displayName}: check the client's local delivery service setup`);
53
+ }
54
+ } else if (native.reasonCode === "native_session_unavailable" && native.runtime !== "active") {
38
55
  steps.push(`${entry.displayName}: open a session connected to the client's local delivery service`);
39
56
  }
40
- return steps;
57
+ return [...new Set(steps)];
41
58
  }
42
59
 
43
60
  // One closed native-delivery report per adapter, built only from detection,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {