agents-can-communicate 0.1.11 → 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.
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +30 -7
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +10 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +36 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -4
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +33 -6
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -156,9 +156,21 @@ async function diagnoseAdapters({ options, runtime }) {
|
|
|
156
156
|
|
|
157
157
|
export async function runDoctor({ options, context, runtime }) {
|
|
158
158
|
const root = context.paths.root;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
// The clock comes from the context rather than through the service, because
|
|
160
|
+
// the service is what may not open: this command runs before anything reads a
|
|
161
|
+
// record, which is the whole point of it.
|
|
162
|
+
const clock = context.clock ?? context.service?.clock;
|
|
163
|
+
// A store that is not there yet is not an ambiguous one. Opening the workspace
|
|
164
|
+
// used to happen first and created it, so the inspection never saw this case;
|
|
165
|
+
// now that the diagnosis runs first, "no protocol.json" would read as
|
|
166
|
+
// "unreadable protocol.json" and a person's first `acc doctor` in a new
|
|
167
|
+
// project would answer that their store is broken.
|
|
168
|
+
const started = await stat(path.join(root, "protocol.json")).then(() => true, () => false);
|
|
169
|
+
const report = !started
|
|
170
|
+
? { healthy: true, blocked: [], corrupt: [], repaired: [] }
|
|
171
|
+
: options.repair === true
|
|
172
|
+
? await repairFilesystemStore({ root, clock })
|
|
173
|
+
: await diagnoseFilesystemStore({ root });
|
|
162
174
|
const adapters = await diagnoseAdapters({ options, runtime });
|
|
163
175
|
const { data: dataHome } = platformPaths({ platform: runtime?.platform,
|
|
164
176
|
env: runtime?.env ?? {} });
|
|
@@ -166,7 +178,7 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
166
178
|
? await runtime.version().catch(() => null)
|
|
167
179
|
: null;
|
|
168
180
|
const update = await noticeUpdate({ dataHome, running, env: runtime?.env ?? {},
|
|
169
|
-
now: Date.parse(
|
|
181
|
+
now: Date.parse(clock.now()), get: runtime?.fetch,
|
|
170
182
|
io: { readFile, writeFile, mkdir } });
|
|
171
183
|
|
|
172
184
|
// Before the store is read for anything else. `collectStatus` reads every
|
|
@@ -185,7 +197,11 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
185
197
|
remediation: ["inspect blocked and corrupt paths before repairing"] });
|
|
186
198
|
}
|
|
187
199
|
|
|
188
|
-
|
|
200
|
+
// Only now, and only because the report said the records can be read. A
|
|
201
|
+
// context built for this command opens the store on request rather than up
|
|
202
|
+
// front.
|
|
203
|
+
const service = context.service ?? await context.openService();
|
|
204
|
+
const status = await service.collectStatus({});
|
|
189
205
|
|
|
190
206
|
const data = {
|
|
191
207
|
workspaceId: context.descriptor.id,
|
|
@@ -210,6 +226,13 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
210
226
|
// the store was healthy and nothing else.
|
|
211
227
|
const text = [`store healthy; ${describePresence(status.counts)}; `
|
|
212
228
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
213
|
-
...data.remediation.map(line => ` ${line}`)
|
|
229
|
+
...data.remediation.map(line => ` ${line}`),
|
|
230
|
+
// `0 of 4` is a true line that reads as a broken machine, and on an
|
|
231
|
+
// MCP-only one it would read that way on every run forever. The server needs
|
|
232
|
+
// no adapter: measured answering `tools/list` and writing an intent on a
|
|
233
|
+
// machine with no client binaries on PATH at all.
|
|
234
|
+
...(installed === 0
|
|
235
|
+
? [" no client is wired; any MCP client can still take part through acc-mcp"]
|
|
236
|
+
: [])].join("\n");
|
|
214
237
|
return { data, text };
|
|
215
238
|
}
|
|
@@ -125,6 +125,16 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
125
125
|
// Said once, where it is needed: the reader has just been shown a list of
|
|
126
126
|
// their own files with ACC's name in them.
|
|
127
127
|
...(action === "install" && acted > 0 ? ["", "undo with: acc uninstall"] : []),
|
|
128
|
+
// And said once where it is needed differently: an install that wired nothing
|
|
129
|
+
// has just told this reader four times that their machine is not supported.
|
|
130
|
+
// It is not - the MCP server needs no adapter, and was measured answering on a
|
|
131
|
+
// machine with no client binaries at all. Only when nothing was wired: the
|
|
132
|
+
// skips already carry their own remedy, and a line printed every time is a
|
|
133
|
+
// line that stops being read.
|
|
134
|
+
...(action === "install" && acted === 0
|
|
135
|
+
? ["", "no client was wired, but any MCP client can still take part: "
|
|
136
|
+
+ "point it at the acc-mcp command"]
|
|
137
|
+
: []),
|
|
128
138
|
].join("\n");
|
|
129
139
|
}
|
|
130
140
|
|
|
@@ -54,7 +54,14 @@ async function claimOn(options, context) {
|
|
|
54
54
|
return mine[0].claimId;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Where this workspace's state lives, without reading a byte of it.
|
|
59
|
+
*
|
|
60
|
+
* Split out because `doctor` is the command a person runs when the store cannot
|
|
61
|
+
* be read, and it has to know which store to look at before deciding whether
|
|
62
|
+
* looking is safe.
|
|
63
|
+
*/
|
|
64
|
+
async function locateContext(options, runtime) {
|
|
58
65
|
const descriptor = await discoverWorkspace({
|
|
59
66
|
cwd: options.cwd ?? runtime.cwd,
|
|
60
67
|
env: runtime.env,
|
|
@@ -67,12 +74,37 @@ async function openContext(options, runtime) {
|
|
|
67
74
|
workspaceId: descriptor.id,
|
|
68
75
|
workspaceRoots: descriptor.roots,
|
|
69
76
|
});
|
|
77
|
+
return { descriptor, paths };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function withService({ descriptor, paths }, runtime) {
|
|
70
81
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
71
82
|
ids: runtime.ids, workspaceId: descriptor.id });
|
|
72
83
|
return { descriptor, paths,
|
|
73
84
|
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
async function openContext(options, runtime) {
|
|
88
|
+
return withService(await locateContext(options, runtime), runtime);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A context for the one command that has to survive an unopenable store.
|
|
93
|
+
*
|
|
94
|
+
* `runDoctor` already refused to read records before diagnosing them - fixed
|
|
95
|
+
* after a truncated file made it answer "invalid JSON record" while the
|
|
96
|
+
* inspection had already found the file and put it in a list nobody saw. The
|
|
97
|
+
* throw then moved earlier: opening the store happens before any handler runs,
|
|
98
|
+
* so one unreadable `protocol.json` killed the diagnosis a frame before the code
|
|
99
|
+
* written to report it. Here the store is opened on request, after the report
|
|
100
|
+
* has said that reading is safe.
|
|
101
|
+
*/
|
|
102
|
+
async function openDiagnosticContext(options, runtime) {
|
|
103
|
+
const located = await locateContext(options, runtime);
|
|
104
|
+
return { ...located, clock: runtime.clock,
|
|
105
|
+
openService: async () => (await withService(located, runtime)).service };
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
const human = value => (typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
77
109
|
|
|
78
110
|
/** How many are here, and how many only look it. */
|
|
@@ -368,7 +400,9 @@ export async function main(argv, runtime) {
|
|
|
368
400
|
// and `help` has to answer in a directory that is no workspace at all.
|
|
369
401
|
const context = NO_WORKSPACE.includes(parsed.command)
|
|
370
402
|
? null
|
|
371
|
-
:
|
|
403
|
+
: parsed.command === "doctor"
|
|
404
|
+
? await openDiagnosticContext(parsed.options, runtime)
|
|
405
|
+
: await openContext(parsed.options, runtime);
|
|
372
406
|
// Which session is calling is answered once, here, rather than by each
|
|
373
407
|
// handler: every one of them needs the same pair, and a handler that forgot
|
|
374
408
|
// to ask would be an operation an agent cannot reach.
|
|
@@ -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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
}
|