agents-can-communicate 0.1.12 → 0.1.13

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": { ".": "./src/index.mjs" },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
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.1.12",
3
+ "version": "0.1.13",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,9 +32,9 @@ export function encode(value) {
32
32
  return Buffer.from(`${serialised}\n`, "utf8");
33
33
  }
34
34
 
35
- async function bytesIfPresent(filePath, root) {
35
+ async function bytesIfPresent(filePath, root, openFile) {
36
36
  try {
37
- return await readRegularNoFollow(filePath, root);
37
+ return await readRegularNoFollow(filePath, root, openFile);
38
38
  } catch (error) {
39
39
  if (error.code === "ENOENT") return null;
40
40
  throw error;
@@ -87,8 +87,10 @@ export async function publishAtomic(destination, bytes, { root, tmpDir, replace
87
87
  }
88
88
  }
89
89
 
90
- export async function readJsonIfPresent(filePath, root) {
91
- const bytes = await bytesIfPresent(filePath, root);
90
+ // The opener is the seam the race tests use, and stays last so callers that do
91
+ // not care never see it.
92
+ export async function readJsonIfPresent(filePath, root, openFile) {
93
+ const bytes = await bytesIfPresent(filePath, root, openFile);
92
94
  if (bytes === null) return null;
93
95
  try {
94
96
  return { value: JSON.parse(bytes.toString("utf8")), bytes };
@@ -22,9 +22,36 @@ function defaultPidIsAlive(pid) {
22
22
  }
23
23
  }
24
24
 
25
- async function readOwner(directory, root) {
26
- const found = await readJsonIfPresent(path.join(directory, OWNER), root);
27
- return found?.value ?? null;
25
+ /**
26
+ * Who holds the lock, or nothing if it moved while we looked.
27
+ *
28
+ * Reads inside the store refuse a parent directory whose identity changed
29
+ * between the check and the open - the defence against a directory being
30
+ * swapped under a read. This lock is the one directory whose entire life is
31
+ * being created and removed: `mkdir` grants it, `rm` releases it, so its inode
32
+ * changes every time it passes from one process to the next. Reading its owner
33
+ * through the strict path meant a contended lock raised
34
+ * "record parent directory changed while opening" and the whole command failed -
35
+ * seen on Linux CI with four agents attaching to one fresh workspace, the
36
+ * ordinary way two people start work.
37
+ *
38
+ * Here that is not an anomaly, it is the answer: the lock moved. Null says so,
39
+ * and both callers already do the right thing with it - the acquiring loop waits
40
+ * and looks again, and the releasing branch declines to remove a directory that
41
+ * is no longer the one it created. The guard itself is untouched, and still
42
+ * refuses everywhere a parent has no business changing.
43
+ */
44
+ async function readOwner(directory, root, openFile) {
45
+ try {
46
+ const found = await readJsonIfPresent(path.join(directory, OWNER), root, openFile);
47
+ return found?.value ?? null;
48
+ } catch (error) {
49
+ if (error instanceof AccError && error.code === EXIT.DATA
50
+ && /parent directory changed/.test(error.message)) {
51
+ return null;
52
+ }
53
+ throw error;
54
+ }
28
55
  }
29
56
 
30
57
  /**
@@ -58,7 +85,7 @@ async function takeStaleOwnership(directory, root, owner, now, pidIsAlive) {
58
85
 
59
86
  export async function withWriterMutex(paths, options, operation) {
60
87
  const { root, clock, pidIsAlive = defaultPidIsAlive, uuid = randomUUID,
61
- attempts = 50, waitMs = 20 } = options;
88
+ attempts = 50, waitMs = 20, openFile } = options;
62
89
  const directory = path.join(paths.locks, "writer.lock");
63
90
  await ensureManagedDirectory(root, paths.locks);
64
91
  const token = uuid();
@@ -68,7 +95,7 @@ export async function withWriterMutex(paths, options, operation) {
68
95
  await mkdir(directory);
69
96
  } catch (error) {
70
97
  if (error.code !== "EEXIST") throw error;
71
- const owner = await readOwner(directory, root);
98
+ const owner = await readOwner(directory, root, openFile);
72
99
  if (!await takeStaleOwnership(directory, root, owner, clock.now(), pidIsAlive)) {
73
100
  await new Promise(resolve => { setTimeout(resolve, waitMs); });
74
101
  }
@@ -79,7 +106,7 @@ export async function withWriterMutex(paths, options, operation) {
79
106
  try {
80
107
  return await operation();
81
108
  } finally {
82
- const current = await readOwner(directory, root);
109
+ const current = await readOwner(directory, root, openFile);
83
110
  if (current?.token === token) await rm(directory, { recursive: true, force: true });
84
111
  }
85
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Local-first coordination for independently opened AI agent sessions.",
6
6
  "keywords": [