agents-can-communicate 0.5.4 → 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 (24) hide show
  1. package/docs/ARCHITECTURE.md +6 -1
  2. package/docs/TROUBLESHOOTING.md +18 -2
  3. package/docs/UPGRADING.md +32 -6
  4. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  5. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  6. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  7. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  8. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  9. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  10. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  11. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  12. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation-files.mjs +25 -0
  13. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +1 -22
  14. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +1 -1
  15. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +2 -2
  16. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +23 -1
  17. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  18. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  19. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  20. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  21. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  23. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  24. package/package.json +1 -1
@@ -174,7 +174,12 @@ The CLI owns installation generations under `<dataHome>/acc/runtime`, outside wo
174
174
  All five launch paths select a generation and publish an actual-process lease under one
175
175
  admission mutex before loading workspace-capable code. Immutable launcher modules and
176
176
  runtime directories preserve in-progress imports. Leases survive `main()` returning and
177
- are removed only after confirmed process death. A session is also pinned to the generation
177
+ are removed only after confirmed process death. Older updaters can omit the store contract
178
+ when publishing a newer generation. The reader recovers missing contracts only from the
179
+ referenced generation, after checking its package identity and content hash, including file
180
+ modes. It preserves explicit declarations and does not rewrite live leases. Missing or
181
+ changed generation files and unknown native bindings remain unverified holds.
182
+ A session is also pinned to the generation
178
183
  it started with: its hooks delegate to that generation while the pin declares the same
179
184
  store contract this one does, and run the active generation otherwise. A generation
180
185
  directory is reclaimed once no control pointer, live lease, live pin or staging hold names
@@ -238,8 +238,9 @@ Next: [Getting started](GETTING_STARTED.md) · [Capabilities](CAPABILITIES.md)
238
238
 
239
239
  Run `acc doctor` to see the update policy and pending notice. A hold keeps an update
240
240
  pending only while the store contract it declares differs from the incoming version's or
241
- is unknown; records written before 0.5.0 declare none, so the first update after upgrading
242
- still waits for all of them. `acc update` names each remaining hold with its process and
241
+ is unknown. ACC can recover a missing contract from the process's unchanged managed
242
+ generation when that generation declares one. A generation that predates the declaration
243
+ still needs its processes to exit. `acc update` names each remaining hold with its process and
243
244
  its declared contract. ACC process leases, including persistent MCP servers, end on
244
245
  confirmed process exit. Native bindings clear on observed SessionEnd or confirmed process
245
246
  death; a vendor daemon may remain alive after SessionEnd. Unknown PIDs remain holds until
@@ -250,6 +251,21 @@ unrelated holds still require lifecycle cleanup or confirmed process exit; safet
250
251
  do not expire merely by elapsed time. See
251
252
  [maintenance and recovery](UPGRADING.md#confirmed-client-service-maintenance).
252
253
 
254
+ If ACC 0.5.3 or 0.5.4 reports `acc-claude-channel; contract unknown` after an earlier
255
+ 0.4.x upgrade, its active pointer may have lost the contract field. This can affect newly
256
+ opened Channels too. A pending old updater retries its downloaded release without looking
257
+ for a newer fix. Install the current global CLI, then use that CLI to complete the update:
258
+
259
+ ```bash
260
+ npm install -g agents-can-communicate@latest
261
+ acc update
262
+ acc doctor
263
+ ```
264
+
265
+ The new reader can activate a compatible release while those Channels remain open.
266
+ It still waits for different contracts, unknown native bindings, or generation files it
267
+ cannot verify. See [legacy contract recovery](UPGRADING.md#recover-missing-runtime-contracts).
268
+
253
269
  Use `acc update` to retry a failed download or finish an interrupted integration refresh.
254
270
  A download failure keeps the working version. A partial integration refresh blocks
255
271
  workspace commands until recovery completes, while hooks let the client continue without
package/docs/UPGRADING.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Upgrading to 0.5.0
2
2
 
3
+ ## Recover missing runtime contracts
4
+
5
+ ACC 0.5.5 fixes an upgrade defect that can leave 0.5.3 or 0.5.4 waiting for
6
+ `acc-claude-channel` processes with `contract unknown`. The 0.4.x updater can activate
7
+ a newer generation without copying its store contract into the active pointer. Channels
8
+ started afterward copy that incomplete pointer even though their runtime declares a contract.
9
+
10
+ For an installation already waiting on an older pending release, run:
11
+
12
+ ```bash
13
+ npm install -g agents-can-communicate@latest
14
+ acc update
15
+ acc version
16
+ acc doctor
17
+ ```
18
+
19
+ The global CLI supplies the new management reader before runtime activation. This step is
20
+ needed because the older updater retries its pending release before checking npm for another
21
+ version. If a version pin selects an older release, clear it or select 0.5.5 first.
22
+
23
+ The reader verifies the referenced generation's package identity, complete contents and
24
+ file modes before recovering a missing contract. Compatible Channels can keep running on
25
+ their original generation during activation. Explicit differing contracts and unknown
26
+ native bindings still block. A generation without a contract, missing files, or changed
27
+ contents remains unknown. ACC does not terminate these processes or edit their lease records.
28
+
3
29
  ## From 0.4.4, 0.4.3, 0.4.2, 0.4.1 or 0.4.0
4
30
 
5
31
  This release keeps the existing workspace data format. It changes when an update is
@@ -56,17 +82,17 @@ only two kinds keep an update pending:
56
82
  - A hold whose declared store contract differs from the incoming version's. This is the
57
83
  case the gate exists for, and a release that changes the store contract still needs
58
84
  every live process to exit.
59
- - A hold that declares no contract, which includes every record written by a release
60
- before this one. Unknown cannot be compared, so it stays a conservative wait.
85
+ - A hold with no verifiable contract. Older native bindings and generations that predate
86
+ the declaration remain unknown. A missing field can be recovered when the referenced
87
+ managed generation declares its contract and passes integrity verification.
61
88
 
62
89
  A hold declaring the same store contract as the incoming version proceeds. Several open
63
90
  Claude Code sessions, a Codex daemon and an idle ACC MCP server no longer have to be
64
91
  closed together to move between releases that share a contract.
65
92
 
66
- The first update after installing this release still waits for every process, because the
67
- holds it has to judge were written before the contract field existed. Close the relevant
68
- clients and persistent ACC processes once, or accept the eligible Codex service
69
- maintenance offer described below. Updates after that do not need it.
93
+ Processes still running a generation from before the contract field existed must exit once.
94
+ Close the relevant clients and persistent ACC processes, or accept the eligible Codex service
95
+ maintenance offer described below. Later compatible runtimes do not require that restart.
70
96
 
71
97
  `acc update` names each remaining hold with its process and its declared contract, so a
72
98
  wait now states its reason rather than only the PID. `acc doctor` reports the same
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.5.4",
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-codex",
3
- "version": "0.5.4",
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",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.5.4",
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-grok",
3
- "version": "0.5.4",
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.4",
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.4",
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.4",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.5.4",
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.4",
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.4",
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.4",
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/mcp-server",
3
- "version": "0.5.4",
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/protocol",
3
- "version": "0.5.4",
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/storage-filesystem",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "accManagedUpdateProtocol": 2,
5
5
  "accStoreVersion": 6,
6
6
  "type": "module",