agents-can-communicate 0.10.0 → 0.10.1

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 (22) hide show
  1. package/docs/ARCHITECTURE.md +7 -2
  2. package/docs/UPGRADING.md +12 -3
  3. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  4. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  5. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  6. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  7. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  8. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  9. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  10. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  11. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  12. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  13. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +78 -4
  14. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +4 -0
  15. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  16. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  17. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  18. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  19. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  20. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  21. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  22. package/package.json +1 -1
@@ -267,8 +267,13 @@ every integration, and only publishes the new active pointer after all applicati
267
267
  remains fenced and repairs forward, with the old active pointer retained but normal
268
268
  workspace admission unavailable until recovery completes. Native bindings cease holding
269
269
  after observed SessionEnd cleanup or confirmed process death; the vendor daemon may remain
270
- running. Unknown PIDs remain holds until lifecycle cleanup. Presence TTL, `acc finish`,
271
- and delivery off alone do not prove native lifecycle end. Normal background updates never
270
+ running. A binding that names no client process is judged by its session record instead
271
+ (#273): it stops holding when the record is gone, when the record names a process that has
272
+ exited, or, with no process recorded, when ACC's presence rule calls the session offline -
273
+ closed, or 30 minutes without a heartbeat. A live recorded process, or a record that cannot be
274
+ read, keeps it a hold. A client that wakes later runs its next hook through the launcher on
275
+ the active generation. For a binding that names its client, presence TTL, `acc finish`, and
276
+ delivery off do not prove native lifecycle end. Normal background updates never
272
277
  restart a vendor daemon. An explicitly approved maintenance job can do so through the
273
278
  adapter lifecycle port: it waits for idle work, validates exact process identity, stops
274
279
  the service under the admission fence, refreshes integrations, and restores and verifies
package/docs/UPGRADING.md CHANGED
@@ -50,6 +50,13 @@ acc doctor
50
50
  - **Sending a message reads less of the store.** A send and its retries use a bounded index
51
51
  instead of rereading unrelated messages and receipts.
52
52
 
53
+ 0.10.1 lets this update finish where 0.10.0 could stay pending. A session that ended without
54
+ naming its client process could leave a native binding that 0.10.0 counted as a live client
55
+ forever, so `acc update` kept reporting `unknown client pid` holds after every session had
56
+ closed. 0.10.1 judges such a binding by its session record and removes it while it prepares
57
+ the update, so the update from 0.9.x finishes once no 0.9.x session runs. From 0.10.0, 0.10.1
58
+ keeps the store contract, so it activates without waiting for open sessions.
59
+
53
60
  ## From 0.8.x
54
61
 
55
62
  Update as usual:
@@ -433,9 +440,11 @@ Running ACC processes, including idle MCP servers, still publish a hold for as l
433
440
  live, and native bindings still hold until observed client SessionEnd cleanup or confirmed
434
441
  process death; the vendor daemon can remain running after that lifecycle event. What
435
442
  changed is which of those holds keeps an update pending: only one whose declared store
436
- contract differs from the incoming version's, or that declares none at all. Unknown PIDs
437
- remain conservative holds until lifecycle cleanup. `acc finish`, presence TTL, and delivery
438
- off alone still do not prove native end. When a hold does block, close the relevant client
443
+ contract differs from the incoming version's, or that declares none at all. From 0.10.1, a
444
+ binding that names no client process stops holding when its session record is gone, names an
445
+ exited process, or, with no process recorded, is offline: closed, or 30 minutes without a
446
+ heartbeat (#273). For a binding that names its client, `acc finish`, presence TTL, and
447
+ delivery off still do not prove native end. When a hold does block, close the relevant client
439
448
  sessions and persistent ACC processes to release it, or accept the eligible Codex service
440
449
  maintenance offer in `acc update`. Background updates never request fresh restart consent.
441
450
  Hooks never wait for a network download. If integration refresh temporarily prevents
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-antigravity",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "acc",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Coordinate this Antigravity CLI session with other AI agent sessions working in the same workspace."
5
5
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,11 +2,12 @@ import { createHash } from "node:crypto";
2
2
  // Kept above 300 lines because activation and generation retirement must use
3
3
  // the same validated runtime/native hold definitions under the admission mutex.
4
4
  // Splitting their safety rules risks disagreeing about which users are live.
5
- import { readdir } from "node:fs/promises";
5
+ import { readdir, rm } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
+ import { classifySessionPresence } from "@agents-can-communicate/core";
8
9
  import { assertPortableId } from "@agents-can-communicate/protocol";
9
- import { readSessionRecord } from "@agents-can-communicate/storage-filesystem";
10
+ import { readSessionRecord, withWriterMutex } from "@agents-can-communicate/storage-filesystem";
10
11
  import { listRuntimeHolds } from "./leases.mjs";
11
12
  import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
12
13
  import { reapPins } from "./pins.mjs";
@@ -45,6 +46,30 @@ async function isMcpContinuity(record, name, workspaceRoot, workspaceId) {
45
46
  } catch { return false; } // Ambiguous or corrupt ownership remains a safety hold.
46
47
  }
47
48
 
49
+ const clientPidOf = record => record?.schemaVersion === 1 && Number.isSafeInteger(record.clientPid)
50
+ && record.clientPid > 0 ? record.clientPid : null;
51
+
52
+ /** A binding that names no client process cannot be judged by its own facts;
53
+ * its session record can. It is stale when the record is gone, when the record
54
+ * names a process that has exited, or, with no process recorded, when ACC's own
55
+ * presence rule calls the session offline - closed, or quiet past the floor for
56
+ * an unknown process - the rule retention removes sessions by. A live recorded
57
+ * process, or a record or store that cannot be read, keeps the binding a hold.
58
+ * On 2026-10-06 the maintainer's 0.10.0 waited behind 32 such bindings from
59
+ * 0.5.10 to 0.9.0 that no client could clear (#273). */
60
+ async function staleWithoutPid(workspaceRoot, workspaceId, record, pidIsAlive) {
61
+ if (record?.schemaVersion !== 1) return false;
62
+ let session;
63
+ try {
64
+ session = await readSessionRecord({ root: workspaceRoot, workspaceId,
65
+ sessionId: record.accSessionId, allowLegacyIdentity: true });
66
+ } catch { return false; }
67
+ if (session === null) return true;
68
+ if (Number.isSafeInteger(session.pid) && session.pid > 0) return confirmedDead(session.pid, pidIsAlive);
69
+ // No process to ask, so the probe is never called.
70
+ return classifySessionPresence(session, new Date().toISOString(), () => true) === "offline";
71
+ }
72
+
48
73
  /** Native bindings outlive finish/TTL; MCP process lifetime is covered by its runtime lease. */
49
74
  export async function listNativeHolds(root, { pidIsAlive = defaultPidIsAlive, strict = false } = {}) {
50
75
  const workspaces = path.join(path.dirname(await canonicalManagerRoot(root)), "workspaces");
@@ -68,9 +93,9 @@ export async function listNativeHolds(root, { pidIsAlive = defaultPidIsAlive, st
68
93
  });
69
94
  if (record === undefined) continue; // Concurrent lifecycle removal, before fencing.
70
95
  if (await isMcpContinuity(record, name, path.dirname(bindings), entry.name)) continue;
71
- const pid = record?.schemaVersion === 1 && Number.isSafeInteger(record.clientPid)
72
- && record.clientPid > 0 ? record.clientPid : null;
96
+ const pid = clientPidOf(record);
73
97
  if (pid !== null && await confirmedDead(pid, pidIsAlive)) continue;
98
+ if (pid === null && await staleWithoutPid(path.dirname(bindings), entry.name, record, pidIsAlive)) continue;
74
99
  holds.push({ kind: "native", pid, reason: pid === null ? "unknown_client_pid" : "client_running",
75
100
  storeVersion: Number.isSafeInteger(record?.storeVersion) ? record.storeVersion : null });
76
101
  }
@@ -78,6 +103,55 @@ export async function listNativeHolds(root, { pidIsAlive = defaultPidIsAlive, st
78
103
  return holds;
79
104
  }
80
105
 
106
+ // The lock a hook holds while it loads, renews and publishes a binding: the
107
+ // hook runner's session lifecycle mutex, keyed by the harness session id.
108
+ const LIFECYCLE_LOCK_MS = 2_000;
109
+ const lifecycleLocks = (workspaceRoot, harnessSessionId) => ({ locks: path.join(workspaceRoot,
110
+ "lifecycle-locks", createHash("sha256").update(String(harnessSessionId)).digest("hex")) });
111
+
112
+ /** Remove the bindings without a client pid that listNativeHolds no longer
113
+ * counts. An older generation activates its successor with its own rule, which
114
+ * counts every one of them as a live client; it loads the pending generation's
115
+ * refresh before it lists blockers, so that refresh calls this (#273). Each one
116
+ * is judged again and removed under its session's lifecycle lock, so a hook that
117
+ * renews it is either seen or comes after (AI review of #274); a lock that stays
118
+ * busy leaves the binding as it is. */
119
+ export async function sweepStaleBindings(root, { pidIsAlive = defaultPidIsAlive } = {}) {
120
+ const workspaces = path.join(path.dirname(await canonicalManagerRoot(root)), "workspaces");
121
+ if (!await managedDirectory(workspaces)) return 0;
122
+ const clock = { now: () => new Date().toISOString() };
123
+ let removed = 0;
124
+ for (const entry of await readdir(workspaces, { withFileTypes: true })) {
125
+ if (!entry.isDirectory()) continue;
126
+ const workspaceRoot = path.join(workspaces, entry.name);
127
+ const bindings = path.join(workspaceRoot, "bindings");
128
+ if (!await managedDirectory(bindings)) continue;
129
+ let changed = false;
130
+ for (const name of await readdir(bindings)) {
131
+ if (!name.endsWith(".json")) continue;
132
+ const file = path.join(bindings, name);
133
+ const stale = async record => record !== undefined && record !== null
134
+ && clientPidOf(record) === null && typeof record.harnessSessionId === "string"
135
+ && !await isMcpContinuity(record, name, workspaceRoot, entry.name)
136
+ && staleWithoutPid(workspaceRoot, entry.name, record, pidIsAlive);
137
+ const judged = await readManagedJson(file).catch(() => undefined);
138
+ if (!await stale(judged)) continue;
139
+ const gone = await withWriterMutex(lifecycleLocks(workspaceRoot, judged.harnessSessionId),
140
+ { root: workspaceRoot, clock, deadlineAt: Date.now() + LIFECYCLE_LOCK_MS }, async () => {
141
+ const current = await readManagedJson(file).catch(() => undefined);
142
+ if (current?.harnessSessionId !== judged.harnessSessionId || !await stale(current)) return false;
143
+ await rm(file, { force: true });
144
+ return true;
145
+ }).catch(() => false);
146
+ if (!gone) continue;
147
+ removed += 1;
148
+ changed = true;
149
+ }
150
+ if (changed) await syncDirectory(bindings);
151
+ }
152
+ return removed;
153
+ }
154
+
81
155
  /** Unknown on either side cannot be compared, and an uncomparable contract
82
156
  * stays a hold. Equal contracts coexist by the store's own rule. */
83
157
  export const blocksActivation = (hold, incomingStoreVersion) =>
@@ -2,12 +2,16 @@ import path from "node:path";
2
2
  import { applyPlan, detectInstallation, livePolicyOf, loadOwnership, planInstallation }
3
3
  from "@agents-can-communicate/installer";
4
4
  import { ALL_ADAPTERS, clientContext, probeTimeout } from "../install-command.mjs";
5
+ import { sweepStaleBindings } from "./activation.mjs";
5
6
  import { stablePaths, writeLaunchers } from "./launchers.mjs";
6
7
  import { bridgeLegacyMaintenance } from "./legacy-maintenance.mjs";
7
8
 
8
9
  /** Loaded from the candidate, so its own adapters supply its integration bytes. */
9
10
  export async function prepareRefresh({ control, root, env = process.env, callerProtocol = 1 }) {
10
11
  await bridgeLegacyMaintenance({ control, root, env, callerProtocol });
12
+ // An older generation lists its blockers right after this returns, by a rule
13
+ // that counts a binding without a client pid as a live client forever (#273).
14
+ await sweepStaleBindings(root).catch(() => {});
11
15
  const dataHome = path.dirname(path.dirname(root));
12
16
  const wanted = new Set(control.targets);
13
17
  const adapters = ALL_ADAPTERS().filter(adapter => wanted.has(adapter.id));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
4
4
  "accManagedUpdateProtocol": 2,
5
5
  "accStoreVersion": 7,
6
6
  "type": "module",