agents-can-communicate 0.7.0 → 0.8.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.
Files changed (97) hide show
  1. package/README.md +3 -1
  2. package/bin/entrypoints/acc-bootstrap.mjs +6 -48
  3. package/bin/entrypoints/acc-claude-channel.mjs +47 -157
  4. package/docs/ADAPTER_AUTHORING.md +41 -10
  5. package/docs/ARCHITECTURE.md +28 -13
  6. package/docs/CAPABILITIES.md +19 -16
  7. package/docs/CLI.md +60 -14
  8. package/docs/CONCEPTS.md +12 -4
  9. package/docs/CONFIGURATION.md +18 -11
  10. package/docs/GETTING_STARTED.md +15 -12
  11. package/docs/GLOSSARY.md +2 -1
  12. package/docs/HOW_IT_WORKS.md +8 -4
  13. package/docs/PROTOCOL.md +56 -6
  14. package/docs/RELEASING.md +3 -2
  15. package/docs/SECURITY_MODEL.md +14 -1
  16. package/docs/TROUBLESHOOTING.md +64 -27
  17. package/docs/UPGRADING.md +49 -8
  18. package/docs/WHY_ACC.md +5 -3
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +8 -0
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +8 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +25 -30
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +254 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
  37. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +8 -0
  39. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  40. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +8 -0
  41. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  43. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +8 -0
  44. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  45. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +8 -0
  46. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  47. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +15 -2
  48. package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
  49. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
  50. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
  51. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
  52. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +23 -11
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +5 -1
  54. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  55. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
  56. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
  57. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
  58. package/node_modules/@agents-can-communicate/cli/src/main.mjs +39 -10
  59. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
  60. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  61. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
  62. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
  63. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  64. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +2 -2
  65. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +21 -4
  66. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +66 -10
  67. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
  68. package/node_modules/@agents-can-communicate/core/src/status.mjs +38 -1
  69. package/node_modules/@agents-can-communicate/core/src/sync.mjs +11 -2
  70. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +24 -7
  72. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  73. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
  74. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +24 -11
  75. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +21 -12
  76. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  77. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
  78. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +3 -7
  79. package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
  80. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
  81. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
  82. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
  83. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
  84. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  85. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +5 -3
  86. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  87. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  88. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
  89. package/package.json +1 -1
  90. package/bin/acc-bootstrap.mjs +0 -7
  91. package/bin/acc-claude-channel.mjs +0 -10
  92. package/bin/entrypoints/claude-channel-binding.mjs +0 -46
  93. package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
  94. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
  95. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
  96. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
  97. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
@@ -0,0 +1,109 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ // What Claude Code does with a wake from a sender that attests no permission
6
+ // mode - ACC never attests one. A session that bypasses permission prompts
7
+ // holds it for approval; `crossSessionInbound` overrides that both ways. ACC
8
+ // reads the same inputs so the sender is told what will happen, and never
9
+ // changes any of them.
10
+
11
+ const MAX_BYTES = 256 * 1024;
12
+ const RECEPTIONS = Object.freeze(["delivered", "held", "refused"]);
13
+
14
+ export const MANAGED_SETTINGS = Object.freeze({
15
+ darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
16
+ linux: "/etc/claude-code/managed-settings.json",
17
+ win32: "C:\\Program Files\\ClaudeCode\\managed-settings.json",
18
+ });
19
+
20
+ async function readSettings(file) {
21
+ try {
22
+ const text = await readFile(file, "utf8");
23
+ if (Buffer.byteLength(text) > MAX_BYTES) return null;
24
+ const value = JSON.parse(text);
25
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ const text = value => (typeof value === "string" && value !== "" ? value : null);
32
+
33
+ /**
34
+ * The two inputs, each from the first file that sets it, in Claude Code's own
35
+ * order: managed policy, then the project's local and shared settings, then
36
+ * the user's.
37
+ */
38
+ export async function readInboundSettings({ configDir, projectDir, managedSettingsPath }) {
39
+ const files = [managedSettingsPath,
40
+ ...(typeof projectDir === "string" && path.isAbsolute(projectDir)
41
+ ? [path.join(projectDir, ".claude", "settings.local.json"),
42
+ path.join(projectDir, ".claude", "settings.json")] : []),
43
+ path.join(configDir, "settings.json")].filter(file => typeof file === "string");
44
+ const layers = await Promise.all(files.map(readSettings));
45
+ const first = pick => layers.map(layer => (layer === null ? null : pick(layer))).find(value => value !== null)
46
+ ?? null;
47
+ return {
48
+ crossSessionInbound: first(layer => text(layer.crossSessionInbound)),
49
+ defaultMode: first(layer => text(layer.permissions?.defaultMode)),
50
+ };
51
+ }
52
+
53
+ /**
54
+ * The permission mode a Claude Code process was started with, from its own
55
+ * arguments, or null. Only the two flags are read; nothing else is kept.
56
+ */
57
+ export function permissionModeFromArgs(args) {
58
+ if (!Array.isArray(args)) return null;
59
+ for (let index = 0; index < args.length; index += 1) {
60
+ const arg = args[index];
61
+ if (arg === "--dangerously-skip-permissions") return "bypassPermissions";
62
+ if (arg === "--permission-mode") return text(args[index + 1]);
63
+ if (typeof arg === "string" && arg.startsWith("--permission-mode=")) {
64
+ return text(arg.slice("--permission-mode=".length));
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /** A process's arguments as `ps` prints them, split on spaces, or null. */
71
+ export function readProcessArgs(pid, { timeoutMs = 500 } = {}) {
72
+ if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32") return Promise.resolve(null);
73
+ return new Promise(resolve => {
74
+ execFile("ps", ["-o", "args=", "-p", String(pid)], { timeout: timeoutMs, maxBuffer: 64 * 1024 },
75
+ (error, stdout) => resolve(error ? null : String(stdout).trim().split(/\s+/).filter(Boolean)));
76
+ });
77
+ }
78
+
79
+ export function receptionOf({ permissionMode, crossSessionInbound }) {
80
+ if (crossSessionInbound === "accept") return "delivered";
81
+ if (crossSessionInbound === "refuse") return "refused";
82
+ if (crossSessionInbound === "hold" || permissionMode === "bypassPermissions") return "held";
83
+ return "delivered";
84
+ }
85
+
86
+ export const isReception = value => RECEPTIONS.includes(value);
87
+
88
+ /**
89
+ * What `acc doctor` tells a person who turned live delivery on: the machine-wide
90
+ * view, from managed policy and the user's settings. A project can still set
91
+ * its own value; the bind reads that one per session.
92
+ */
93
+ export async function inboundStatus({ configDir, managedSettingsPath }) {
94
+ const userFile = path.join(configDir, "settings.json");
95
+ for (const file of [managedSettingsPath, userFile].filter(item => typeof item === "string")) {
96
+ const value = text((await readSettings(file))?.crossSessionInbound);
97
+ if (value === "accept") return { state: "accepted", diagnostic: null };
98
+ if (value === "refuse") {
99
+ return { state: "refused", diagnostic: `crossSessionInbound is "refuse" in ${file}, so `
100
+ + "Claude Code drops every ACC wake; messages arrive with each next turn; set it to "
101
+ + "\"accept\" for live delivery" };
102
+ }
103
+ if (value !== null) break;
104
+ }
105
+ return { state: "attention", diagnostic: "a session that bypasses permission prompts asks "
106
+ + `before each ACC wake; add "crossSessionInbound": "accept" to ${userFile} to take wakes `
107
+ + "without asking; the next turn shows the message either way" };
108
+ }
109
+
@@ -7,13 +7,14 @@ import path from "node:path";
7
7
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
- import { acccreatedFile, bakeSkillCommand, blankJson, defaultBootstrap, defaultChannel,
10
+ import { acccreatedFile, bakeSkillCommand, blankJson,
11
11
  mergeOwnedEntries, ownedEntries,
12
12
  keepVersions, ownVersion, stampPluginVersion,
13
13
  removeIfEmpty,
14
14
  removeInstalledTree,
15
15
  removeOwnedEntries, writeCliShim, writeForeignJson, writeHookShim }
16
16
  from "@agents-can-communicate/adapter-sdk";
17
+ import { MANAGED_SETTINGS, inboundStatus } from "./inbox-settings.mjs";
17
18
 
18
19
  const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
19
20
  const manifest = fileURLToPath(new URL("../plugin/.claude-plugin/plugin.json",
@@ -22,9 +23,6 @@ const manifest = fileURLToPath(new URL("../plugin/.claude-plugin/plugin.json",
22
23
  const PLUGIN_NAME = "agents-can-communicate";
23
24
  const MARKETPLACE = "acc-local";
24
25
  const QUALIFIED = `${PLUGIN_NAME}@${MARKETPLACE}`;
25
- const CHANNEL_ACTIVATION_CHECK = "Check Claude's Channels startup notice for ACC; "
26
- + "an MCP connection does not verify inbound delivery. If Channels are unavailable "
27
- + "or blocked, resolve that client warning before expecting live messages.";
28
26
 
29
27
  /**
30
28
  * How this client actually installs a plugin.
@@ -147,24 +145,21 @@ const marketplaceManifest = () => ({
147
145
  }],
148
146
  });
149
147
 
150
- // The channel MCP entry Claude Code loads only when the session is started with
151
- // the captured development-channel flag. Written with the pinned Node and the
152
- // installed Channel binary, so the generated file carries no repository path;
153
- // removed entirely for a non-live install so a plain launch spawns nothing.
148
+ // Up to 0.7.x a live install wrote the Channel MCP server here, and Claude
149
+ // Code spawned it for every session that enabled the plugin. The inbox wake
150
+ // needs no MCP server, so no plugin copy ACC keeps may name one.
154
151
  const mcpPath = target => path.join(target, ".mcp.json");
155
- async function writeChannelMcp(target, { node, channel }) {
156
- await writeJson(mcpPath(target), { mcpServers: { "acc-channel": {
157
- command: node, args: [channel] } } });
152
+
153
+ async function removeChannelConfig(versionsRoot) {
154
+ const versions = await readdir(versionsRoot).catch(() => []);
155
+ await Promise.all(versions.map(version => rm(mcpPath(path.join(versionsRoot, version)),
156
+ { force: true })));
158
157
  }
159
158
 
160
159
  /** A plugin tree with the shim written and the skill's command baked in. */
161
- async function layOutPlugin(target, { runner, node, cli, channel, live }) {
160
+ async function layOutPlugin(target, { runner, node, cli }) {
162
161
  await rm(target, { recursive: true, force: true });
163
162
  await cp(bundle, target, { recursive: true });
164
- // The bundle ships a placeholder .mcp.json; the real one is written only for a
165
- // live install, and a non-live tree carries none.
166
- await rm(mcpPath(target), { force: true });
167
- if (live) await writeChannelMcp(target, { node, channel });
168
163
  // The skill ships with a placeholder where the command belongs: `acc` is not
169
164
  // on PATH everywhere, and an agent that cannot run it improvises. The shim
170
165
  // carries the pinning so each example can name one path. No data home is
@@ -181,9 +176,8 @@ async function layOutPlugin(target, { runner, node, cli, channel, live }) {
181
176
  version: await pluginVersion(), io: { readFile, writeFile } });
182
177
  }
183
178
 
184
- export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null, node = process.execPath,
185
- channel = defaultChannel(), livePolicy = "off", now = new Date() }) {
186
- const live = livePolicy === "actionable" || livePolicy === "all";
179
+ export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null,
180
+ node = process.execPath, now = new Date() }) {
187
181
  // Everything this will merge into, read before a byte is written. A settings
188
182
  // file that will not parse used to be discovered after the plugin tree was
189
183
  // already on disk, and the install then failed with nineteen files left
@@ -200,12 +194,12 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
200
194
  const source = sourceDir(configDir);
201
195
  const cached = cachePath(configDir, version);
202
196
 
203
- await layOutPlugin(source, { runner, node, cli, channel, live });
197
+ await layOutPlugin(source, { runner, node, cli });
204
198
  await writeJson(marketplaceFile(configDir), marketplaceManifest());
205
199
  // The copy the client runs from. Written here rather than asking the user to
206
200
  // run `claude plugin install`, exactly as the Codex adapter does, because the
207
201
  // command's only effect is this copy plus the two registry entries below.
208
- await layOutPlugin(cached, { runner, node, cli, channel, live });
202
+ await layOutPlugin(cached, { runner, node, cli });
209
203
  // The copy just written, plus the one an upgrade moved off. A client caches a
210
204
  // plugin under its version, so every upgrade would otherwise leave the previous
211
205
  // release's tree beside this one - invisible while the version never moved,
@@ -214,6 +208,10 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
214
208
  // anything older than that holds no session and is litter.
215
209
  await keepVersions({ root: path.dirname(cached), keep: [version, keepPreviousVersion],
216
210
  io: { readdir, rm } });
211
+ // The copy an upgrade moved off stays for sessions already running from it,
212
+ // but its Channel config would still start a Channel for any session that
213
+ // reads it. Those sessions keep what they loaded; nothing new starts one.
214
+ await removeChannelConfig(path.dirname(cached));
217
215
 
218
216
  await writeClientJson(knownMarketplacesPath(configDir), {
219
217
  ...known,
@@ -254,12 +252,9 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
254
252
 
255
253
  return { ok: true,
256
254
  changes: [source, cached, marketplaceFile(configDir),
257
- knownMarketplacesPath(configDir), installedPluginsPath(configDir), file,
258
- ...(live ? [mcpPath(source), mcpPath(cached)] : [])],
259
- needsAction: live ? [CHANNEL_ACTIVATION_CHECK] : [],
260
- diagnostics: live
261
- ? ["native channel wired; Claude's experimental development-channel warning still applies"]
262
- : [] };
255
+ knownMarketplacesPath(configDir), installedPluginsPath(configDir), file],
256
+ needsAction: [],
257
+ diagnostics: [] };
263
258
  }
264
259
 
265
260
  export async function uninstallClaudePlugin({ configDir, keep = [] }) {
@@ -297,16 +292,15 @@ export async function uninstallClaudePlugin({ configDir, keep = [] }) {
297
292
  return { ok: true, changes, diagnostics: [] };
298
293
  }
299
294
 
300
- export async function detectClaude({ configDir }) {
295
+ export async function detectClaude({ configDir, managedSettingsPath = MANAGED_SETTINGS[process.platform] }) {
301
296
  const settings = await readJson(settingsPath(configDir), null);
302
297
  const enabled = settings?.enabledPlugins?.[QUALIFIED] === true;
303
298
  const registered = Object.hasOwn(
304
299
  (await readJson(installedPluginsPath(configDir), { plugins: {} })).plugins ?? {},
305
300
  QUALIFIED);
306
- const channelConfigured = enabled && registered
307
- && (await readJson(mcpPath(sourceDir(configDir)), null))?.mcpServers?.["acc-channel"] != null;
308
301
  return { ok: true, changes: [],
309
- needsAction: channelConfigured ? [CHANNEL_ACTIVATION_CHECK] : [],
302
+ inboundDelivery: await inboundStatus({ configDir, managedSettingsPath }),
303
+ needsAction: [],
310
304
  diagnostics: [enabled && registered
311
305
  ? "acc plugin registered and enabled"
312
306
  : "acc plugin not registered"] };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -111,6 +111,10 @@ the communication obligation rather than proving the requested action is complet
111
111
  An acknowledgement confirms receipt, not acceptance of work. Read the peer's
112
112
  answer for the scope it accepted or the result it actually checked.
113
113
 
114
+ To see where a message you sent got to, read it exactly:
115
+ `{{ACC}} sync --scope history --message <id> --json` lists each recipient's
116
+ receipt state, when it last changed, and the transport that offered it.
117
+
114
118
  ## Read and answer only your inbox
115
119
 
116
120
  Plain `{{ACC}} inbox` returns `{items, nextCursor}`: pending message headers,
@@ -125,6 +129,10 @@ Pages default to 20 items and stay within 12,000 bytes of formatted page JSON.
125
129
  Use `inbox --cursor <nextCursor>` for older headers when needed. Omit the cursor
126
130
  on a new poll to see arrivals; a cursor is the complete last message id, not an offset.
127
131
 
132
+ A peer block with a `repeat:` line was pushed live earlier and nothing has
133
+ retrieved it since. A note you already acted on needs nothing more; for a
134
+ message that asks for one, a reply or acknowledgement is still owed.
135
+
128
136
  An injected peer block is already the message body. If context was compacted,
129
137
  or a body did not fit, retrieve exactly the named message:
130
138
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -111,6 +111,10 @@ the communication obligation rather than proving the requested action is complet
111
111
  An acknowledgement confirms receipt, not acceptance of work. Read the peer's
112
112
  answer for the scope it accepted or the result it actually checked.
113
113
 
114
+ To see where a message you sent got to, read it exactly:
115
+ `{{ACC}} sync --scope history --message <id> --json` lists each recipient's
116
+ receipt state, when it last changed, and the transport that offered it.
117
+
114
118
  ## Read and answer only your inbox
115
119
 
116
120
  Plain `{{ACC}} inbox` returns `{items, nextCursor}`: pending message headers,
@@ -125,6 +129,10 @@ Pages default to 20 items and stay within 12,000 bytes of formatted page JSON.
125
129
  Use `inbox --cursor <nextCursor>` for older headers when needed. Omit the cursor
126
130
  on a new poll to see arrivals; a cursor is the complete last message id, not an offset.
127
131
 
132
+ A peer block with a `repeat:` line was pushed live earlier and nothing has
133
+ retrieved it since. A note you already acted on needs nothing more; for a
134
+ message that asks for one, a reply or acknowledgement is still owed.
135
+
128
136
  An injected peer block is already the message body. If context was compacted,
129
137
  or a body did not fit, retrieve exactly the named message:
130
138
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -127,6 +127,10 @@ the communication obligation rather than proving the requested action is complet
127
127
  An acknowledgement confirms receipt, not acceptance of work. Read the peer's
128
128
  answer for the scope it accepted or the result it actually checked.
129
129
 
130
+ To see where a message you sent got to, read it exactly:
131
+ `{{ACC}} sync --scope history --message <id> --json` lists each recipient's
132
+ receipt state, when it last changed, and the transport that offered it.
133
+
130
134
  ## Read and answer only your inbox
131
135
 
132
136
  Plain `{{ACC}} inbox` returns `{items, nextCursor}`: pending message headers,
@@ -141,6 +145,10 @@ Pages default to 20 items and stay within 12,000 bytes of formatted page JSON.
141
145
  Use `inbox --cursor <nextCursor>` for older headers when needed. Omit the cursor
142
146
  on a new poll to see arrivals; a cursor is the complete last message id, not an offset.
143
147
 
148
+ A peer block with a `repeat:` line was pushed live earlier and nothing has
149
+ retrieved it since. A note you already acted on needs nothing more; for a
150
+ message that asks for one, a reply or acknowledgement is still owed.
151
+
144
152
  An injected peer block is already the message body. If context was compacted,
145
153
  or a body did not fit, retrieve exactly the named message:
146
154
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -111,6 +111,10 @@ the communication obligation rather than proving the requested action is complet
111
111
  An acknowledgement confirms receipt, not acceptance of work. Read the peer's
112
112
  answer for the scope it accepted or the result it actually checked.
113
113
 
114
+ To see where a message you sent got to, read it exactly:
115
+ `{{ACC}} sync --scope history --message <id> --json` lists each recipient's
116
+ receipt state, when it last changed, and the transport that offered it.
117
+
114
118
  ## Read and answer only your inbox
115
119
 
116
120
  Plain `{{ACC}} inbox` returns `{items, nextCursor}`: pending message headers,
@@ -125,6 +129,10 @@ Pages default to 20 items and stay within 12,000 bytes of formatted page JSON.
125
129
  Use `inbox --cursor <nextCursor>` for older headers when needed. Omit the cursor
126
130
  on a new poll to see arrivals; a cursor is the complete last message id, not an offset.
127
131
 
132
+ A peer block with a `repeat:` line was pushed live earlier and nothing has
133
+ retrieved it since. A note you already acted on needs nothing more; for a
134
+ message that asks for one, a reply or acknowledgement is still owed.
135
+
128
136
  An injected peer block is already the message body. If context was compacted,
129
137
  or a body did not fit, retrieve exactly the named message:
130
138
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,7 +51,18 @@ function offeredBreadcrumb(item, liveOfferedMessageIds) {
51
51
  + `\`acc inbox --message ${item.sourceId}\``;
52
52
  }
53
53
 
54
- function messageGroups(messages) {
54
+ // A body shown again because a live transport accepted it and nothing followed.
55
+ // Said inside the block so the model can match it to what it may already have.
56
+ function repeatLines(message, repeats) {
57
+ const repeat = repeats.get(message.messageId);
58
+ // Both values come from the store, and the store is shared with every peer's
59
+ // process, so they are escaped like any other line inside the block.
60
+ return repeat === undefined ? []
61
+ : [oneLine(escapePeerText(`repeat: offered via ${repeat.transport} at ${repeat.at}; `
62
+ + "no retrieval recorded"))];
63
+ }
64
+
65
+ function messageGroups(messages, repeats = new Map()) {
55
66
  return messages.map(message => ({
56
67
  messageId: message.messageId,
57
68
  kind: "message",
@@ -63,6 +74,7 @@ function messageGroups(messages) {
63
74
  `messageId: ${message.messageId}`,
64
75
  `sender: ${message.fromParticipantId} (session ${message.fromSessionId})`,
65
76
  `obligation: ${message.obligation}`,
77
+ ...repeatLines(message, repeats),
66
78
  ...decisionLines(message).map(escapePeerText),
67
79
  `subject: ${oneLine(escapePeerText(message.subject))}`,
68
80
  "body:",
@@ -129,7 +141,8 @@ export function projectContextResult(sync, { budgetBytes = DEFAULT_BUDGET_BYTES
129
141
  const liveOfferedMessageIds = new Set(sync.liveOfferedMessageIds ?? []);
130
142
  const groups = [
131
143
  ...attentionGroups(urgent, { truncatable: true, liveOfferedMessageIds }),
132
- ...messageGroups(messages),
144
+ ...messageGroups(messages, new Map((sync.repeatOffers ?? [])
145
+ .map(repeat => [repeat.messageId, repeat]))),
133
146
  ...reminderGroups(attention.filter(isReminder)),
134
147
  ...attentionGroups(informational, { truncatable: false }),
135
148
  ];
@@ -4,7 +4,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
4
4
  // so the four cannot drift: a consumer that has to ask which adapter produced an
5
5
  // event is a consumer that will eventually get it wrong.
6
6
  export const NORMALIZED_EVENT_KEYS = Object.freeze(["kind", "sessionId", "cwd", "model",
7
- "parentSessionId", "tool", "targets"]);
7
+ "parentSessionId", "tool", "targets", "permissionMode", "endReason"]);
8
8
 
9
9
  export const EVENT_KINDS = Object.freeze(["sessionStart", "sessionEnd", "heartbeat",
10
10
  "beforeTurn", "beforeTool", "afterTool", "turnEnd", "childStart", "childEnd", "other"]);
@@ -34,7 +34,7 @@ export function normalizedEvent(fields) {
34
34
  }
35
35
  }
36
36
  const { kind, sessionId, cwd, model = null, parentSessionId = null, tool = null,
37
- targets = [] } = fields;
37
+ targets = [], permissionMode = null, endReason = null } = fields;
38
38
 
39
39
  if (!EVENT_KINDS.includes(kind)) data("unknown normalised event kind", { kind });
40
40
  if (typeof sessionId !== "string" || sessionId === "") {
@@ -50,6 +50,19 @@ export function normalizedEvent(fields) {
50
50
  }
51
51
  }
52
52
 
53
+ // The client's own name for the session's permission mode, when its hook
54
+ // reports one. Only a native binding reads it, to tell a sender whether the
55
+ // client will hold a wake for approval.
56
+ if (permissionMode !== null && (typeof permissionMode !== "string" || permissionMode === "")) {
57
+ data("normalised event permission mode must be a non-empty string", { kind });
58
+ }
59
+
60
+ // Why the client ended the session, in its own words, when it says. A sender
61
+ // is told a conversation was cleared rather than that its peer vanished.
62
+ if (endReason !== null && (typeof endReason !== "string" || endReason === "")) {
63
+ data("normalised event end reason must be a non-empty string", { kind });
64
+ }
65
+
53
66
  return Object.freeze({ kind, sessionId, cwd, model, parentSessionId, tool,
54
- targets: Object.freeze([...targets]) });
67
+ targets: Object.freeze([...targets]), permissionMode, endReason });
55
68
  }
@@ -58,8 +58,6 @@ const ownBinary = name => {
58
58
  };
59
59
 
60
60
  export const defaultRunner = () => ownBinary("acc-hook.mjs");
61
- export const defaultBootstrap = () => ownBinary("acc-bootstrap.mjs");
62
- export const defaultChannel = () => ownBinary("acc-claude-channel.mjs");
63
61
  export const defaultAntigravityRelay = () => ownBinary("acc-antigravity-relay.mjs");
64
62
 
65
63
  export const runnerExists = async runner => {
@@ -199,7 +197,10 @@ export async function writeCliShim({ dir, cli = defaultCli(), node = process.exe
199
197
  * created, and a recognised path is not a reason to delete it.
200
198
  */
201
199
  export async function removeInstalledTree(target, keep = []) {
202
- if (keep.includes(target)) return false;
200
+ // A kept tree holds someone's own work. Removing a directory inside it, or one
201
+ // that contains it, would delete that work as surely as removing it.
202
+ const within = (inner, outer) => inner === outer || inner.startsWith(`${outer}${path.sep}`);
203
+ if (keep.some(kept => within(target, kept) || within(kept, target))) return false;
203
204
  try {
204
205
  await lstat(target);
205
206
  } catch (error) {
@@ -8,7 +8,7 @@ export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE
8
8
  validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
9
9
  from "./native-delivery.mjs";
10
10
  export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
11
- export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultBootstrap, defaultChannel, defaultCli,
11
+ export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli,
12
12
  defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
13
13
  from "./hook-shim.mjs";
14
14
  export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { COMMAND_NAME, IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
3
+ import { IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
4
4
  closed, deepFreeze, isPlainObject, isText, usage } from "./native-vocabulary.mjs";
5
5
 
6
6
  // A native activation plan is what an adapter hands the installer: closed
@@ -27,19 +27,9 @@ function validateMechanism(mechanism, index) {
27
27
  usage(`${label} kind must be one of ${NATIVE_ACTIVATION_KINDS.join(", ")}`);
28
28
  }
29
29
  if (mechanism.kind === "shell-bootstrap") {
30
- closed(mechanism, ["kind", "command", "realExecutable", "prefixArgs"], label);
31
- if (!isText(mechanism.command) || !COMMAND_NAME.test(mechanism.command)) {
32
- usage(`${label} command must be a bare command name`);
33
- }
34
- if (!isText(mechanism.realExecutable) || !path.isAbsolute(mechanism.realExecutable)) {
35
- usage(`${label} realExecutable must be an absolute path`);
36
- }
37
- if (!Array.isArray(mechanism.prefixArgs) || mechanism.prefixArgs.some(arg => !isText(arg)
38
- || SHELL_SOURCE.test(arg))) {
39
- usage(`${label} prefixArgs must be plain argument strings, never shell source`);
40
- }
41
- return { kind: mechanism.kind, command: mechanism.command,
42
- realExecutable: mechanism.realExecutable, prefixArgs: [...mechanism.prefixArgs] };
30
+ // ACC 0.7.x put a `claude` shim on PATH this way. Install records of it are
31
+ // still read so it can be retired; a plan can no longer create one.
32
+ usage(`${label}: shell-bootstrap activation was removed; no adapter may plan one`);
43
33
  }
44
34
  if (mechanism.kind === "native-config") {
45
35
  closed(mechanism, ["kind", "artifactIds"], label);
@@ -1,5 +1,5 @@
1
- import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_ACTIVATION_KINDS,
2
- NATIVE_BINDING_MODES, NATIVE_PLATFORMS, PROBE_KEYS, TIMESTAMP, assertModes,
1
+ import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_BINDING_MODES,
2
+ NATIVE_PLATFORMS, PLANNABLE_ACTIVATION_KINDS, PROBE_KEYS, TIMESTAMP, assertModes,
3
3
  assertReasonCode, closed, compareStableVersions, deepFreeze, isPlainObject, isText,
4
4
  parseStableVersion, usage } from "./native-vocabulary.mjs";
5
5
 
@@ -12,19 +12,31 @@ import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_ACTI
12
12
  // capability; this rule is used for native live delivery alone.
13
13
 
14
14
  export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
15
- compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
15
+ PLANNABLE_ACTIVATION_KINDS, compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
16
16
  export { validateNativeActivationPlan } from "./native-activation.mjs";
17
17
 
18
18
  const orderedModes = modes => NATIVE_BINDING_MODES.filter(mode => modes.includes(mode));
19
19
 
20
20
  export function validateNativeDeliveryContract(value, { certification, client }) {
21
+ // Consent to live delivery is what `acc install` recorded. Up to 0.7.x a
22
+ // Claude shell shim could export it instead ("bootstrap-environment"); the
23
+ // shim is gone, and the installation record is the one source left.
21
24
  const policySource = Object.hasOwn(value ?? {}, "policySource")
22
- ? value.policySource : "bootstrap-environment";
23
- closed({ ...value, policySource },
24
- ["minimumByPlatform", "anchors", "knownBad", "activationKinds", "policySource"],
25
+ ? value.policySource : "installation-record";
26
+ // What a successful offer put in front of the model. "message" carries the
27
+ // body, so the router records the receipt as offered. "wake" carries only a
28
+ // notice that makes the client run a turn; the body reaches the model through
29
+ // the next-turn hook, which records the offer itself once its stdout carried
30
+ // the body. Recording a wake as offered would hide the body from that hook.
31
+ const offerKind = Object.hasOwn(value ?? {}, "offerKind") ? value.offerKind : "message";
32
+ closed({ ...value, policySource, offerKind },
33
+ ["minimumByPlatform", "anchors", "knownBad", "activationKinds", "policySource", "offerKind"],
25
34
  "nativeDelivery");
26
- if (!["installation-record", "bootstrap-environment"].includes(policySource)) {
27
- usage("nativeDelivery.policySource must be installation-record or bootstrap-environment");
35
+ if (policySource !== "installation-record") {
36
+ usage("nativeDelivery.policySource must be installation-record");
37
+ }
38
+ if (!["message", "wake"].includes(offerKind)) {
39
+ usage("nativeDelivery.offerKind must be message or wake");
28
40
  }
29
41
  const minimums = value.minimumByPlatform;
30
42
  if (!isPlainObject(minimums) || Object.keys(minimums).length === 0) {
@@ -89,11 +101,11 @@ export function validateNativeDeliveryContract(value, { certification, client })
89
101
  });
90
102
  const kinds = value.activationKinds;
91
103
  if (!Array.isArray(kinds) || kinds.length === 0 || new Set(kinds).size !== kinds.length
92
- || kinds.some(kind => !NATIVE_ACTIVATION_KINDS.includes(kind))) {
93
- usage(`nativeDelivery.activationKinds must be unique entries of ${NATIVE_ACTIVATION_KINDS.join(", ")}`);
104
+ || kinds.some(kind => !PLANNABLE_ACTIVATION_KINDS.includes(kind))) {
105
+ usage(`nativeDelivery.activationKinds must be unique entries of ${PLANNABLE_ACTIVATION_KINDS.join(", ")}`);
94
106
  }
95
107
  return deepFreeze({ minimumByPlatform: { ...minimums }, anchors, knownBad,
96
- activationKinds: [...kinds], policySource });
108
+ activationKinds: [...kinds], policySource, offerKind });
97
109
  }
98
110
 
99
111
  function knownBadHit(contract, version) {
@@ -9,10 +9,15 @@ export const NATIVE_BINDING_MODES = Object.freeze([
9
9
  "livePush", "idleWake", "busyQueue", "replyRoute",
10
10
  ]);
11
11
 
12
+ // Every kind an install record may carry. "shell-bootstrap" is the `claude`
13
+ // shim ACC 0.7.x wrote; records of it stay readable so it can be retired.
12
14
  export const NATIVE_ACTIVATION_KINDS = Object.freeze([
13
15
  "shell-bootstrap", "native-config", "native-service",
14
16
  ]);
15
17
 
18
+ // The kinds an adapter may still ask for.
19
+ export const PLANNABLE_ACTIVATION_KINDS = Object.freeze(["native-config", "native-service"]);
20
+
16
21
  export const NATIVE_PLATFORMS = Object.freeze([
17
22
  "darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-x64",
18
23
  ]);
@@ -34,7 +39,6 @@ export const NATIVE_REASON_CODES = Object.freeze([
34
39
  export const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
35
40
  export const CONTRACT_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
36
41
  export const IDENTIFIER = /^[a-z][a-z0-9_-]*$/;
37
- export const COMMAND_NAME = /^[a-z][a-z0-9_.-]*$/;
38
42
  export const FINGERPRINT = /^sha256:[0-9a-f]{64}$/;
39
43
  export const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
40
44
  export const SHELL_SOURCE = /[;&|<>`\n\0]|\$\(/;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {