agents-can-communicate 0.5.2 → 0.5.4

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 (50) hide show
  1. package/README.md +8 -5
  2. package/docs/CLI.md +19 -5
  3. package/docs/CONCEPTS.md +7 -0
  4. package/docs/CONFIGURATION.md +7 -3
  5. package/docs/GETTING_STARTED.md +19 -3
  6. package/docs/MCP.md +5 -1
  7. package/docs/PROTOCOL.md +17 -3
  8. package/docs/TROUBLESHOOTING.md +7 -7
  9. package/docs/UPGRADING.md +7 -4
  10. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  11. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +57 -10
  12. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  13. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +57 -10
  14. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +5 -2
  15. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +2 -1
  16. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +1 -1
  17. package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +37 -12
  18. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +1 -1
  19. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +2 -1
  20. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +135 -0
  21. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +57 -10
  23. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  24. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  25. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +57 -10
  26. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  27. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +57 -10
  28. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  29. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  30. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +10 -5
  31. package/node_modules/@agents-can-communicate/cli/src/help.mjs +10 -3
  32. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +15 -71
  33. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +152 -0
  34. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +6 -1
  35. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +22 -5
  36. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +8 -8
  38. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  40. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  41. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +6 -2
  42. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +11 -0
  43. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -1
  44. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -2
  45. package/node_modules/@agents-can-communicate/installer/src/service-setup.mjs +30 -0
  46. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  47. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +8 -5
  48. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  49. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  50. package/package.json +1 -1
@@ -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}`] : [])]),
@@ -34,7 +34,7 @@ const SUMMARY = Object.freeze({
34
34
  work: "publish what this session is doing, or --clear when it has stopped",
35
35
  claim: "reserve a resource; exit 5 when someone else already holds it",
36
36
  release: "give a claim back",
37
- ack: "answer a message that asked for one, so it stops asking",
37
+ ack: "acknowledge receipt without accepting work or writing an answer",
38
38
  message: "send a typed message to named participants",
39
39
  inbox: "list pending message headers; --message reads one complete message",
40
40
  reply: "reply to one message and acknowledge it in the same operation",
@@ -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."],
@@ -98,8 +101,12 @@ const NOTES = Object.freeze({
98
101
  "With history, --type filters before paging; --cursor takes the complete message ID in nextCursor.",
99
102
  "history --type decision --current lists terminal decisions and withdrawals, including conflicts. Current means no recorded successor, not consensus.",
100
103
  "history --message reads one complete historical message without changing receipts; do not combine with cursor, limit, or type."],
101
- reply: ["Returns the outgoing message/delivery plus the original acknowledged receipt."],
102
- finish: ["Default status: partial. Records a handoff, releases claims, and closes this ACC session."],
104
+ ack: ["An unanswered question/request requires reply; ack refuses without changing its receipt.",
105
+ "Acknowledgement does not prevent a later reply."],
106
+ reply: ["Returns the outgoing message/delivery plus the original acknowledged receipt.",
107
+ "May follow ack or an earlier answer. Reuse a client-message-id only for the same send retry."],
108
+ finish: ["Default status: partial. Records a handoff, releases claims, and closes this ACC session.",
109
+ "Status describes the original goal; neither partial nor an acknowledged receipt means the peer accepted work."],
103
110
  config: ["init writes optional workspace configuration; validate only checks it.",
104
111
  "Use acc config <subcommand> --help for its options."],
105
112
  update: ["Run acc update to install the latest release and refresh client integrations.",
@@ -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
+ }
@@ -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
  }
@@ -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.2",
3
+ "version": "0.5.4",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -110,6 +110,12 @@ export function createInboxService(ports, sessions) {
110
110
  const now = clock.now();
111
111
  return store.transaction(async tx => {
112
112
  await requireOpen(input, "acknowledge a message", tx);
113
+ const { message, receipt } = requireOwnedReceipt(tx, session, input.messageId);
114
+ if (message.obligation === "reply" && receipt.state !== "acknowledged") {
115
+ throw new AccError(EXIT.CONFLICT,
116
+ "this message requires a reply; use reply to answer, clarify, or decline",
117
+ { messageId: input.messageId, obligation: message.obligation });
118
+ }
113
119
  return advanceOwned(tx, session, input.messageId, "acknowledged", now).receipt;
114
120
  }, { kinds: ["session", "message", "receipt"] });
115
121
  }
@@ -120,14 +126,8 @@ export function createInboxService(ports, sessions) {
120
126
  const replyId = ids.next("message");
121
127
  return store.transaction(tx => {
122
128
  const original = requireOwnedReceipt(tx, session, input.messageId);
123
- const matchingReply = tx.list("message", message =>
124
- message.workspaceId === session.workspaceId
125
- && message.fromParticipantId === session.participantId
126
- && message.clientMessageId === input.clientMessageId).at(0);
127
- if (original.receipt.state === "acknowledged" && matchingReply === undefined) {
128
- throw new AccError(EXIT.CONFLICT, "that message is already resolved",
129
- { messageId: input.messageId });
130
- }
129
+ // A receipt settles delivery, not the conversation. Distinct replies may
130
+ // follow an ack or an earlier answer; recording still enforces retry keys.
131
131
  const recorded = recordMessageInTransaction({ tx, session, now, messageId: replyId, ids,
132
132
  action: "reply to a message", input: {
133
133
  clientMessageId: input.clientMessageId,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
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.2",
3
+ "version": "0.5.4",
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.2",
3
+ "version": "0.5.4",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,3 +1,4 @@
1
+ import { applyServiceSetup } from "./service-setup.mjs";
1
2
  import { applyNativeActivation, deactivateNative } from "./native-activation.mjs";
2
3
  import { finalizeRemoval, missingArtifactParents, recordInstall,
3
4
  removeEmptyOwnedDirectories, removeOwnedArtifacts } from "./ownership.mjs";
@@ -68,7 +69,8 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
68
69
  await recordInstall({ dataHome, adapterId: adapter.id,
69
70
  version: operation.clientVersion ?? null, accVersion,
70
71
  artifacts: operation.artifacts, createdDirectories,
71
- deliveryPolicy: operation.livePolicy, nativeActivation: native });
72
+ deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
73
+ nativeActivation: native });
72
74
  results.operations.push({ ...operation, applied: true, appendedRcBlock,
73
75
  needsAction: outcome.needsAction ?? [],
74
76
  changes: outcome.changes ?? [], diagnostics: [
@@ -77,6 +79,8 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
77
79
  ...(outcome.diagnostics ?? []),
78
80
  ...notes,
79
81
  ] });
82
+ await applyServiceSetup({ adapter, context: installContext,
83
+ operation: results.operations.at(-1), results });
80
84
  } else {
81
85
  // Config ownership can veto removal. Ask before teardown or recorded
82
86
  // artifact deletion; an adapter-local check would run after those writes.
@@ -131,7 +135,7 @@ function describeTeardown(report) {
131
135
  for (const service of report.services) {
132
136
  lines.push(service.outcome === "stopped" ? `stopped the ${service.serviceId} service`
133
137
  : `retained the ${service.serviceId} service (${service.outcome === "retained_pre_existing"
134
- ? "it existed before ACC" : "no vendor teardown exists"})`);
138
+ ? "it was reused at activation" : "no vendor teardown exists"})`);
135
139
  }
136
140
  return lines;
137
141
  }
@@ -145,6 +145,17 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
145
145
  : unsupported(adapter.nativeDelivery === undefined
146
146
  ? "native_delivery_unsupported" : "version_unavailable");
147
147
 
148
+ if (entry.present && typeof adapter.inspectNativeServiceSetup === "function") {
149
+ try {
150
+ entry.nativeServiceSetup = await withTimeout(Promise.resolve().then(() =>
151
+ adapter.inspectNativeServiceSetup({ ...context, platform, clientVersion: entry.version })),
152
+ probeTimeoutMs, `${adapter.id} service setup probe`);
153
+ } catch {
154
+ entry.nativeServiceSetup = { state: "blocked", reasonCode: "service_inspection_failed",
155
+ diagnostic: `${adapter.displayName} service preparation could not be inspected; check the vendor service and retry install` };
156
+ }
157
+ }
158
+
148
159
  try {
149
160
  const detected = await adapter.detect({ ...context, clientVersion: entry.version,
150
161
  platform, nativeDelivery: entry.nativeDelivery });
@@ -110,7 +110,8 @@ async function saveOwnership({ dataHome, record }) {
110
110
  * runtime, and leaves the bundle inside the client exactly where it was.
111
111
  */
112
112
  export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
113
- artifacts, createdDirectories = [], deliveryPolicy, nativeActivation = null }) {
113
+ artifacts, createdDirectories = [], deliveryPolicy, deliveryDecision,
114
+ nativeActivation = null }) {
114
115
  const stamped = await Promise.all(artifacts.map(async artifact => ({
115
116
  path: artifact.path,
116
117
  kind: artifact.kind ?? "file",
@@ -120,6 +121,7 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
120
121
  })));
121
122
  const record = await loadOwnership({ dataHome });
122
123
  const previous = record.installs.find(install => install.adapterId === adapterId);
124
+ const retainedDeliveryDecision = deliveryDecision ?? previous?.deliveryDecision;
123
125
  const directories = [...new Set([
124
126
  ...(previous?.createdDirectories ?? []), ...createdDirectories,
125
127
  ])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length
@@ -129,6 +131,9 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
129
131
  { adapterId, version, accVersion, artifacts: stamped,
130
132
  ...(directories.length === 0 ? {} : { createdDirectories: directories }),
131
133
  ...(deliveryPolicy === undefined ? {} : { deliveryPolicy }),
134
+ ...(retainedDeliveryDecision === undefined ? {} : {
135
+ deliveryDecision: retainedDeliveryDecision,
136
+ }),
132
137
  // Present only for a consented native activation. A record without it
133
138
  // - every 0.2 install - reads as live policy off and is never rewritten
134
139
  // merely to add the field.
@@ -14,7 +14,7 @@ import { LIVE_POLICIES, describeActivation, describeDeactivation, planActivation
14
14
  */
15
15
  export function planInstallation({ adapters, detected, context, action = "install",
16
16
  recorded = [], accVersion = null, allowDowngrade = false, requested = [],
17
- deliveryByAdapter = {} }) {
17
+ deliveryByAdapter = {}, deliveryDecisionByAdapter = {}, allowServiceSetup = false }) {
18
18
  if (!["install", "uninstall"].includes(action)) {
19
19
  throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
20
20
  }
@@ -104,6 +104,9 @@ export function planInstallation({ adapters, detected, context, action = "instal
104
104
  // and so what will be removed. Asking the adapter instead would describe an
105
105
  // install for a machine this one no longer is.
106
106
  const delivery = deliveryByAdapter[entry.adapterId] ?? "off";
107
+ const deliveryDecision = deliveryDecisionByAdapter[entry.adapterId]
108
+ ?? recordedById.get(entry.adapterId)?.deliveryDecision
109
+ ?? { source: "legacy-unknown", completeSetup: false };
107
110
  const native = entry.nativeDelivery ?? null;
108
111
  const liveDeliverySupported = native?.state === "eligible"
109
112
  && native.activationPlan?.eligible === true;
@@ -122,8 +125,10 @@ export function planInstallation({ adapters, detected, context, action = "instal
122
125
  ? describeInstallDelivery(entry, delivery, effectiveLivePolicy) : null;
123
126
  const installContext = { ...context, requestedLivePolicy: delivery,
124
127
  livePolicy: configuredLivePolicy, clientVersion: entry.version, platform: entry.platform };
128
+ const nativeServiceSetup = action === "install" && allowServiceSetup && delivery !== "off"
129
+ && deliveryDecision.completeSetup === true ? entry.nativeServiceSetup : undefined;
125
130
  const setupNotes = action === "install" && delivery !== "off"
126
- ? [entry.outgoingDelivery?.setup, entry.nativeSetup].filter(note => typeof note === "string") : [];
131
+ ? [entry.outgoingDelivery?.setup, nativeServiceSetup ? null : entry.nativeSetup].filter(note => typeof note === "string") : [];
127
132
  // A consented activation that this run keeps, activates, or takes back.
128
133
  // Only an explicit off or an uninstall removes one; an absent record never
129
134
  // creates one.
@@ -154,11 +159,13 @@ export function planInstallation({ adapters, detected, context, action = "instal
154
159
  alreadyInstalled: entry.installed === true,
155
160
  livePolicy: delivery,
156
161
  effectiveLivePolicy,
162
+ ...(action === "install" ? { deliveryDecision } : {}),
157
163
  ...(retainedActivation ? { configuredLivePolicy, retainedNativeActivation: retainedActivation } : {}),
158
164
  ...(deliveryDiagnostic === null ? {} : { deliveryDiagnostic }),
159
165
  ...(deliverySummary === null ? {} : { deliverySummary }),
160
166
  ...(setupNotes.length ? { setupNotes } : {}),
161
167
  ...(nativeActivation === null ? {} : { nativeActivation }),
168
+ ...(nativeServiceSetup ? { nativeServiceSetup } : {}),
162
169
  ...(deactivation === null ? {} : { deactivation }),
163
170
  artifacts,
164
171
  // Said in the operator's terms, not in paths: which files ACC creates
@@ -169,6 +176,7 @@ export function planInstallation({ adapters, detected, context, action = "instal
169
176
  ...(deliverySummary === null ? [] : [deliverySummary]),
170
177
  ...(retainedActivation ? ["keep existing native delivery setup; current readiness is unverified"] : []),
171
178
  ...setupNotes,
179
+ ...(nativeServiceSetup ? [nativeServiceSetup.diagnostic] : []),
172
180
  ...artifacts.filter(a => a.kind === "tree")
173
181
  .map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
174
182
  ...artifacts.filter(a => a.kind === "merge")