agents-can-communicate 0.8.5 → 0.9.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.
- package/README.md +11 -7
- package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
- package/bin/entrypoints/acc-hook.mjs +33 -19
- package/bin/entrypoints/hook-output.mjs +14 -5
- package/docs/ADAPTER_AUTHORING.md +29 -0
- package/docs/CAPABILITIES.md +7 -6
- package/docs/CLI.md +5 -0
- package/docs/CONFIGURATION.md +41 -10
- package/docs/DESIGN_DECISIONS.md +0 -2
- package/docs/GETTING_STARTED.md +39 -6
- package/docs/HOW_IT_WORKS.md +13 -8
- package/docs/SECURITY_MODEL.md +32 -5
- package/docs/TROUBLESHOOTING.md +76 -8
- package/docs/UPGRADING.md +47 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/allow-rule.mjs +129 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
- package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +36 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/src/own-version.mjs +27 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
- package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
- package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
- package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
- package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
- package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
- 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/protocol/src/config.mjs +6 -2
- package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
- package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
- package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
- package/package.json +1 -5
|
@@ -50,13 +50,66 @@ import { diagnoseFilesystemStore, repairFilesystemStore }
|
|
|
50
50
|
*
|
|
51
51
|
* Null for anything unreadable. "Might be old" on every run is not a diagnosis.
|
|
52
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* The package directory of the runner a shim or config names, or null.
|
|
55
|
+
*
|
|
56
|
+
* ACC writes the runner in double quotes in every form: a shell shim
|
|
57
|
+
* (`ACC_RUNNER="..."`), the Node shim Windows gets (a JSON string) and Kimi's
|
|
58
|
+
* config (a TOML string holding a command, whose own quotes are cmd's on
|
|
59
|
+
* Windows and TOML's elsewhere). Each quoted string is read the way a shell
|
|
60
|
+
* reads one: a backslash escapes only a quote, a backslash, $ or a backtick,
|
|
61
|
+
* which is all the shim escapes and all a path needs undone in JSON or TOML,
|
|
62
|
+
* so an unescaped C:\Users\... in a shell script stays that path. A string
|
|
63
|
+
* holding quotes is a command. A Windows path in it is cmd's, which has no
|
|
64
|
+
* escape character, so it is read as it stands and a UNC path keeps its leading
|
|
65
|
+
* \\; other words are read the shell's way, since Kimi's POSIX command escapes a
|
|
66
|
+
* quote in a path; then, when no word names the runner, the string is read
|
|
67
|
+
* whole, since a POSIX path may hold a quote.
|
|
68
|
+
* An apostrophe, as in C:\Users\O'Neil, is part of a path. An unquoted path is
|
|
69
|
+
* read to the whitespace before it.
|
|
70
|
+
*/
|
|
71
|
+
const RUNNER = /[\\/]agents-can-communicate(?=[\\/]bin[\\/]acc-hook\.mjs)/;
|
|
72
|
+
const absoluteRoot = value => {
|
|
73
|
+
const match = RUNNER.exec(value);
|
|
74
|
+
if (match === null) return null;
|
|
75
|
+
const root = value.slice(0, match.index + match[0].length);
|
|
76
|
+
return /^(?:[A-Za-z]:)?[\\/]/.test(root) ? root : null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const quotedStrings = text => [...text.matchAll(/"((?:[^"\\]|\\[\s\S])*)"/g)]
|
|
80
|
+
.map(([, quoted]) => quoted.replace(/\\(["\\$`])/g, "$1"));
|
|
81
|
+
|
|
82
|
+
const commandRoot = command => {
|
|
83
|
+
for (const [, word] of command.matchAll(/"([^"]*)"/g)) {
|
|
84
|
+
const root = /^(?:[A-Za-z]:\\|\\\\)/.test(word) ? absoluteRoot(word) : null;
|
|
85
|
+
if (root !== null) return root;
|
|
86
|
+
}
|
|
87
|
+
for (const word of quotedStrings(command)) {
|
|
88
|
+
const root = absoluteRoot(word);
|
|
89
|
+
if (root !== null) return root;
|
|
90
|
+
}
|
|
91
|
+
return absoluteRoot(command);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function runnerRoot(text) {
|
|
95
|
+
for (const value of quotedStrings(String(text))) {
|
|
96
|
+
const root = value.includes("\"") ? commandRoot(value) : absoluteRoot(value);
|
|
97
|
+
if (root !== null) return root;
|
|
98
|
+
}
|
|
99
|
+
const flat = String(text);
|
|
100
|
+
const match = RUNNER.exec(flat);
|
|
101
|
+
if (match === null) return null;
|
|
102
|
+
const start = flat.slice(0, match.index).search(/\S+$/);
|
|
103
|
+
return start < 0 ? null : absoluteRoot(flat.slice(start));
|
|
104
|
+
}
|
|
105
|
+
|
|
53
106
|
export async function wiredVersion(shimPath) {
|
|
54
107
|
if (typeof shimPath !== "string" || shimPath === "") return null;
|
|
55
108
|
const text = await readFile(shimPath, "utf8").catch(() => null);
|
|
56
109
|
if (text === null) return null;
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
const manifest = await readFile(path.join(
|
|
110
|
+
const root = runnerRoot(text);
|
|
111
|
+
if (root === null) return null;
|
|
112
|
+
const manifest = await readFile(path.join(root, "package.json"), "utf8")
|
|
60
113
|
.catch(() => null);
|
|
61
114
|
if (manifest === null) return null;
|
|
62
115
|
try {
|
|
@@ -103,7 +156,10 @@ async function findShims(root, depth) {
|
|
|
103
156
|
for (const entry of entries) {
|
|
104
157
|
const target = path.join(root, entry.name);
|
|
105
158
|
if (entry.isDirectory()) found.push(...await findShims(target, depth - 1));
|
|
106
|
-
|
|
159
|
+
// The shell shim on POSIX, the Node shim on Windows.
|
|
160
|
+
else if (/^acc-(?:hook|cli)\.(?:sh|mjs)$/.test(entry.name) || entry.name.endsWith(".sh")) {
|
|
161
|
+
found.push(target);
|
|
162
|
+
}
|
|
107
163
|
}
|
|
108
164
|
return found;
|
|
109
165
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// What a hook needs from this package, without the rest of it. A hook starts a
|
|
2
|
+
// process on every turn, and the package index loads the whole command line and
|
|
3
|
+
// the installer with it: modules a hook never runs, about a millisecond each on
|
|
4
|
+
// windows-latest.
|
|
5
|
+
export { resolveHookWorkspace } from "./hook-workspace.mjs";
|
|
6
|
+
export { createGitProbe } from "./git-probe.mjs";
|
|
7
|
+
export { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
8
|
+
export { clearPin, hookEntrypointFor, resolvePinnedGeneration, writePin }
|
|
9
|
+
from "./managed-runtime/pins.mjs";
|
|
@@ -12,7 +12,7 @@ import { runtimePaths } from "./runtime-paths.mjs";
|
|
|
12
12
|
// this survives client exit: resuming the same native conversation keeps its
|
|
13
13
|
// room. Adapter + native id distinguish independent conversations.
|
|
14
14
|
export async function resolveHookWorkspace({ adapterId, event, room, cwd, dataHome, env = {},
|
|
15
|
-
gitProbe, clock, deadlineAt }) {
|
|
15
|
+
gitProbe, clock, deadlineAt, platform = process.platform }) {
|
|
16
16
|
const identityKey = (adapter, native) => createHash("sha256")
|
|
17
17
|
.update(JSON.stringify([adapter, native])).digest("hex");
|
|
18
18
|
if (room !== undefined && !/^[a-f0-9]{64}$/.test(room)) {
|
|
@@ -72,7 +72,13 @@ export async function resolveHookWorkspace({ adapterId, event, room, cwd, dataHo
|
|
|
72
72
|
let current = descriptor;
|
|
73
73
|
if (descriptor.source === "git" && path.resolve(event.cwd) !== workspaceCwd) {
|
|
74
74
|
const git = await gitProbe({ cwd: event.cwd }).catch(() => null);
|
|
75
|
-
|
|
75
|
+
// Both sides canonical: Git spells the directory the way the cwd did
|
|
76
|
+
// (through a symlink, or `C:/…` on Windows), and the room kept that
|
|
77
|
+
// spelling.
|
|
78
|
+
const same = git !== null && await Promise.all([git.commonDir, descriptor.git.commonDir]
|
|
79
|
+
.map(directory => realpath(directory).catch(() => null)))
|
|
80
|
+
.then(([here, room]) => here !== null && here === room);
|
|
81
|
+
if (same) {
|
|
76
82
|
current = { ...descriptor, git };
|
|
77
83
|
}
|
|
78
84
|
}
|
|
@@ -97,13 +103,16 @@ export async function resolveHookWorkspace({ adapterId, event, room, cwd, dataHo
|
|
|
97
103
|
const raced = await read();
|
|
98
104
|
if (raced !== null) return discover(raced);
|
|
99
105
|
deadline();
|
|
106
|
+
// On Windows the session this hook opens next flushes, and that commits
|
|
107
|
+
// this name with NTFS's journal; a crash before it leaves the room to be
|
|
108
|
+
// chosen again by the session's next hook.
|
|
100
109
|
await writeManagedJson(file, { schemaVersion: 1, adapterId,
|
|
101
110
|
nativeSessionId: event.sessionId, cwd: context.workspaceCwd,
|
|
102
111
|
workspaceId: context.descriptor.id, source: context.descriptor.source,
|
|
103
112
|
...(context.descriptor.source === "git" ? { git: {
|
|
104
113
|
commonDir: await realpath(context.descriptor.git.commonDir),
|
|
105
114
|
worktreeRoot: await realpath(context.descriptor.git.worktreeRoot),
|
|
106
|
-
} } : {}) });
|
|
115
|
+
} } : {}) }, { flushName: platform !== "win32" });
|
|
107
116
|
return { ...context, workspaceRef: `acc://${key}` };
|
|
108
117
|
});
|
|
109
118
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readdir
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
@@ -8,6 +8,7 @@ import { listRuntimeHolds } from "./leases.mjs";
|
|
|
8
8
|
import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
|
|
9
9
|
import { reapPins } from "./pins.mjs";
|
|
10
10
|
import { reapStagingHolds } from "./staging.mjs";
|
|
11
|
+
import { removeTree } from "./portable-fs.mjs";
|
|
11
12
|
import { canonicalManagerRoot, managedDirectory, readControl, readManagedJson, syncDirectory, writeControl } from "./state.mjs";
|
|
12
13
|
|
|
13
14
|
/** The staged generation states its own contract in its manifest, so the
|
|
@@ -250,7 +251,7 @@ export async function reclaimGenerations({ root, active = null, pidIsAlive = def
|
|
|
250
251
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
251
252
|
const directory = path.join(generations, entry.name);
|
|
252
253
|
if (referenced.has(directory)) continue;
|
|
253
|
-
await
|
|
254
|
+
await removeTree(directory);
|
|
254
255
|
removed.push(entry.name);
|
|
255
256
|
}
|
|
256
257
|
if (removed.length) await syncDirectory(generations);
|
|
@@ -38,10 +38,19 @@ export async function fetchRelease({ pin = null, env = process.env, get = fetch
|
|
|
38
38
|
return { version: body.version, integrity, registry: registry.href };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/** npm's own script, beside this node first. POSIX's `npm` links to it; on
|
|
42
|
+
* Windows that name is a shell script, and node would run it as JavaScript. */
|
|
43
|
+
export async function npmCli(env, { platform = process.platform, execPath = process.execPath,
|
|
44
|
+
realpath: resolve = realpath } = {}) {
|
|
45
|
+
const flavour = platform === "win32" ? path.win32 : path.posix;
|
|
46
|
+
// A copy of Windows' environment keeps the variable's own spelling, `Path`.
|
|
47
|
+
const search = Object.entries(env).find(([name]) => name.toUpperCase() === "PATH")?.[1] ?? "";
|
|
48
|
+
const directories = [flavour.dirname(execPath), ...search.split(flavour.delimiter)];
|
|
43
49
|
for (const directory of directories.filter(Boolean)) {
|
|
44
|
-
|
|
50
|
+
const candidate = platform === "win32"
|
|
51
|
+
? flavour.join(directory, "node_modules", "npm", "bin", "npm-cli.js")
|
|
52
|
+
: flavour.join(directory, "npm");
|
|
53
|
+
try { return await resolve(candidate); } catch { /* Try the next installed npm. */ }
|
|
45
54
|
}
|
|
46
55
|
throw new Error("npm is unavailable; the working ACC runtime was kept");
|
|
47
56
|
}
|
|
@@ -17,12 +17,14 @@ export const RETIRED_ENTRY_KINDS = Object.freeze(["acc-bootstrap", "acc-claude-c
|
|
|
17
17
|
|
|
18
18
|
// Bootstrap resolution deliberately needs only Node built-ins: no selected
|
|
19
19
|
// generation is imported until admission and its actual PID lease are durable.
|
|
20
|
+
// The same rule as platformDataHome, which this module may not import.
|
|
20
21
|
export function managerLocation({ env = process.env, platform = process.platform } = {}) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const flavour = platform === "win32" ? path.win32 : path.posix;
|
|
23
|
+
const data = env.ACC_DATA_HOME || (platform === "win32" ? env.LOCALAPPDATA
|
|
24
|
+
: platform === "darwin" ? env.HOME && flavour.join(env.HOME, "Library", "Application Support")
|
|
25
|
+
: env.XDG_DATA_HOME || env.HOME && flavour.join(env.HOME, ".local", "share"));
|
|
26
|
+
if (typeof data !== "string" || !flavour.isAbsolute(data)) throw new Error("cannot resolve ACC data home");
|
|
27
|
+
return flavour.join(data, "acc", "runtime");
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function invokedDirectly(url) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath
|
|
1
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath }
|
|
2
2
|
from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { collectFile, fingerprint } from "./generation-files.mjs";
|
|
6
|
+
import { removeTree, renameEntry } from "./portable-fs.mjs";
|
|
6
7
|
import { attachStagingTemp, holdStagedGeneration, releaseStagingHold } from "./staging.mjs";
|
|
8
|
+
import { syncDirectory } from "./state.mjs";
|
|
7
9
|
|
|
8
10
|
const NAME = "agents-can-communicate";
|
|
9
11
|
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
@@ -144,19 +146,17 @@ export async function stageOwnGeneration({ packageRoot, managerRoot }) {
|
|
|
144
146
|
} finally { await handle.close(); }
|
|
145
147
|
}
|
|
146
148
|
for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
|
|
147
|
-
|
|
148
|
-
try { await handle.sync(); } finally { await handle.close(); }
|
|
149
|
+
await syncDirectory(directory);
|
|
149
150
|
}
|
|
150
151
|
try {
|
|
151
|
-
await
|
|
152
|
+
await renameEntry(staging, target);
|
|
152
153
|
} catch (error) {
|
|
153
154
|
if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !await existingMatches(target, files)) throw error;
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
-
try { await handle.sync(); } finally { await handle.close(); }
|
|
156
|
+
await syncDirectory(generations);
|
|
157
157
|
return { ...result, hold };
|
|
158
158
|
} finally {
|
|
159
|
-
await
|
|
159
|
+
await removeTree(staging);
|
|
160
160
|
}
|
|
161
161
|
} catch (error) {
|
|
162
162
|
await releaseStagingHold(hold);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdtemp, open, readFile,
|
|
2
|
+
import { lstat, mkdtemp, open, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ENTRY_KINDS, RETIRED_ENTRY_KINDS } from "./entry.mjs";
|
|
5
|
+
import { removeTree, renameEntry, renameReplacing } from "./portable-fs.mjs";
|
|
5
6
|
import { managedDirectory, syncDirectory } from "./state.mjs";
|
|
6
7
|
|
|
7
8
|
const MODULES = ["entry.mjs", "command-prefix.mjs", "state.mjs", "generation-files.mjs",
|
|
8
|
-
"mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
|
|
9
|
+
"mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs", "portable-fs.mjs"];
|
|
9
10
|
async function durableFile(file, bytes, mode = 0o600) {
|
|
10
11
|
const handle = await open(file, "wx", mode);
|
|
11
12
|
try { await handle.writeFile(bytes); await handle.sync(); }
|
|
@@ -34,7 +35,7 @@ export async function writeLaunchers(root, packageRoot) {
|
|
|
34
35
|
try {
|
|
35
36
|
for (const [name, bytes] of modules) await durableFile(path.join(staging, name), bytes);
|
|
36
37
|
await syncDirectory(staging);
|
|
37
|
-
try { await
|
|
38
|
+
try { await renameEntry(staging, directory); }
|
|
38
39
|
catch (error) { if (!["EEXIST", "ENOTEMPTY"].includes(error.code)) throw error; }
|
|
39
40
|
await managedDirectory(directory);
|
|
40
41
|
for (const [name, bytes] of modules) {
|
|
@@ -43,7 +44,7 @@ export async function writeLaunchers(root, packageRoot) {
|
|
|
43
44
|
|| !(await readFile(file)).equals(bytes)) throw new Error("managed launcher integrity changed");
|
|
44
45
|
}
|
|
45
46
|
await syncDirectory(parent);
|
|
46
|
-
} finally { await
|
|
47
|
+
} finally { await removeTree(staging); }
|
|
47
48
|
const bin = path.join(root, "bin");
|
|
48
49
|
await managedDirectory(bin, { create: true });
|
|
49
50
|
for (const kind of ENTRY_KINDS) {
|
|
@@ -52,7 +53,9 @@ export async function writeLaunchers(root, packageRoot) {
|
|
|
52
53
|
const temporary = path.join(bin, `.${randomUUID()}.tmp`);
|
|
53
54
|
try {
|
|
54
55
|
await durableFile(temporary, bytes, 0o700);
|
|
55
|
-
|
|
56
|
+
// A launcher starting right now may still hold this file open; Windows
|
|
57
|
+
// refuses the replacement until it lets go.
|
|
58
|
+
await renameReplacing(temporary, path.join(bin, `${kind}.mjs`));
|
|
56
59
|
} finally { await rm(temporary, { force: true }); }
|
|
57
60
|
}
|
|
58
61
|
// A 0.7.x `claude` shim or plugin copy that still reaches for one of these
|
|
@@ -75,7 +75,7 @@ export async function spawnMaintenance({ root, job, packageRoot, env }) {
|
|
|
75
75
|
throw new Error("maintenance worker implementation is unavailable");
|
|
76
76
|
}
|
|
77
77
|
const child = spawn(process.execPath, [path.join(packageRoot, "bin", "acc-maintenance-worker.mjs"), root, job.id],
|
|
78
|
-
{ env, cwd: root, detached: true, stdio: "ignore" });
|
|
78
|
+
{ env, cwd: root, detached: true, windowsHide: true, stdio: "ignore" });
|
|
79
79
|
await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
|
|
80
80
|
child.unref();
|
|
81
81
|
return child.pid;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readdir
|
|
2
|
+
import { lstat, mkdir, readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
|
+
import { removeTree, renameEntry } from "./portable-fs.mjs";
|
|
5
6
|
import { canonicalManagerRoot, managedDirectory, readManagedJson, syncDirectory, writeManagedJson } from "./state.mjs";
|
|
6
7
|
|
|
7
8
|
export function defaultPidIsAlive(pid) {
|
|
@@ -49,7 +50,7 @@ async function readOwner(directory) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
async function retire(directory, target) {
|
|
52
|
-
try { await
|
|
53
|
+
try { await renameEntry(directory, target); return true; }
|
|
53
54
|
catch (error) {
|
|
54
55
|
if (["ENOENT", "EEXIST", "ENOTEMPTY"].includes(error.code)) return false;
|
|
55
56
|
throw error;
|
|
@@ -81,10 +82,10 @@ async function compactQuiescentLocks(root, pidIsAlive) {
|
|
|
81
82
|
const tombstone = path.join(root, name);
|
|
82
83
|
const owner = await readOwner(tombstone);
|
|
83
84
|
if (owner && name === `manager.reclaimed-${identity(owner)}.lock`) {
|
|
84
|
-
await
|
|
85
|
+
await removeTree(tombstone);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
for (const candidate of dead) await
|
|
88
|
+
for (const candidate of dead) await removeTree(candidate);
|
|
88
89
|
await syncDirectory(root);
|
|
89
90
|
} catch {
|
|
90
91
|
// Maintenance is best effort. Unreadable/malformed preparation keeps the
|
|
@@ -118,7 +119,7 @@ export async function withManagerLock(root, operation, { timeoutMs = 1000, pidIs
|
|
|
118
119
|
}
|
|
119
120
|
} else {
|
|
120
121
|
try {
|
|
121
|
-
await
|
|
122
|
+
await renameEntry(candidate, directory);
|
|
122
123
|
owned = true;
|
|
123
124
|
await syncDirectory(root);
|
|
124
125
|
await compactQuiescentLocks(root, pidIsAlive);
|
|
@@ -140,6 +141,6 @@ export async function withManagerLock(root, operation, { timeoutMs = 1000, pidIs
|
|
|
140
141
|
const released = path.join(root, `manager.reclaimed-${identity(owner)}.lock`);
|
|
141
142
|
if (await retire(directory, released)) await syncDirectory(root);
|
|
142
143
|
}
|
|
143
|
-
} else await
|
|
144
|
+
} else await removeTree(candidate);
|
|
144
145
|
}
|
|
145
146
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Filesystem calls whose meaning differs on Windows, each measured on a
|
|
2
|
+
// windows-latest runner (docs/design/2026-09-30-native-windows-support.md).
|
|
3
|
+
// POSIX keeps the single call it always made; the Windows branch keeps the rule
|
|
4
|
+
// the caller relies on. Node built-ins only: the managed runtime's launchers
|
|
5
|
+
// carry a byte-identical copy of this file, and they may import nothing else.
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
const BUSY = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
10
|
+
// Long enough for a reader, an antivirus scan or an indexer to let go; short
|
|
11
|
+
// enough that a hook's budget is spent on the write, not on waiting.
|
|
12
|
+
const DEFAULT_WAIT_MS = 2_000;
|
|
13
|
+
const BACKOFF_MS = [5, 10, 20, 40, 80];
|
|
14
|
+
const STEADY_MS = 100;
|
|
15
|
+
|
|
16
|
+
const sleepFor = duration => new Promise(resolve => { setTimeout(resolve, duration); });
|
|
17
|
+
|
|
18
|
+
export const isWindows = (platform = process.platform) => platform === "win32";
|
|
19
|
+
|
|
20
|
+
async function retrying(operation, retry, { deadlineAt, sleep = sleepFor }) {
|
|
21
|
+
const deadline = deadlineAt ?? Date.now() + DEFAULT_WAIT_MS;
|
|
22
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
23
|
+
try {
|
|
24
|
+
return await operation();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const remaining = deadline - Date.now();
|
|
27
|
+
if (remaining <= 0 || !await retry(error)) throw error;
|
|
28
|
+
await sleep(Math.min(BACKOFF_MS[attempt] ?? STEADY_MS, remaining));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rename a file over an existing one.
|
|
35
|
+
*
|
|
36
|
+
* Windows refuses the rename while any handle is open on the target - an ACC
|
|
37
|
+
* reader, an antivirus scan, an indexer - whatever sharing that handle allows.
|
|
38
|
+
* The handle is always short-lived, so the rename is retried until the caller's
|
|
39
|
+
* deadline rather than failing a write whose bytes are ready.
|
|
40
|
+
*/
|
|
41
|
+
export async function renameReplacing(from, to, { platform = process.platform,
|
|
42
|
+
rename = fs.rename, deadlineAt, sleep } = {}) {
|
|
43
|
+
if (!isWindows(platform)) return rename(from, to);
|
|
44
|
+
return retrying(() => rename(from, to), error => BUSY.has(error.code), { deadlineAt, sleep });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rename a directory, or move any entry to a name nobody else uses.
|
|
49
|
+
*
|
|
50
|
+
* POSIX reports an occupied name as EEXIST or ENOTEMPTY, and callers treat that
|
|
51
|
+
* as "somebody else got there first". Windows reports it as EPERM, the same code
|
|
52
|
+
* it uses when a file inside the source is open. The target tells them apart:
|
|
53
|
+
* if it exists the answer is EEXIST, as on POSIX; if not, the source was busy
|
|
54
|
+
* and the rename is retried.
|
|
55
|
+
*/
|
|
56
|
+
export async function renameEntry(from, to, { platform = process.platform,
|
|
57
|
+
rename = fs.rename, lstat = fs.lstat, deadlineAt, sleep } = {}) {
|
|
58
|
+
if (!isWindows(platform)) return rename(from, to);
|
|
59
|
+
return retrying(async () => {
|
|
60
|
+
try {
|
|
61
|
+
return await rename(from, to);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!BUSY.has(error.code)) throw error;
|
|
64
|
+
const occupied = await lstat(to).then(() => true, missing => {
|
|
65
|
+
if (missing.code === "ENOENT") return false;
|
|
66
|
+
throw missing;
|
|
67
|
+
});
|
|
68
|
+
if (occupied) {
|
|
69
|
+
throw Object.assign(new Error(`EEXIST: file already exists, rename '${from}' -> '${to}'`),
|
|
70
|
+
{ code: "EEXIST", syscall: "rename", path: from, dest: to });
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}, error => BUSY.has(error.code), { deadlineAt, sleep });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gone = (file, lstat) => lstat(file).then(() => false,
|
|
78
|
+
error => error.code === "ENOENT");
|
|
79
|
+
|
|
80
|
+
const refusedLink = file => Object.assign(
|
|
81
|
+
new Error(`ELOOP: too many symbolic links encountered, open '${file}'`),
|
|
82
|
+
{ code: "ELOOP", syscall: "open", path: file });
|
|
83
|
+
|
|
84
|
+
// How many times a name that keeps changing hands is opened afresh.
|
|
85
|
+
const SWAPS = 3;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Open a file without following a symlink at its final component.
|
|
89
|
+
*
|
|
90
|
+
* Windows has no O_NOFOLLOW: the flag is undefined there and an open through a
|
|
91
|
+
* file symlink succeeds. The rule is kept by checking the name first, refusing a
|
|
92
|
+
* symlink or junction (Node reports both as symbolic links), and after the open
|
|
93
|
+
* checking the name again: it must be no link, and name the very file the handle
|
|
94
|
+
* holds. A name another process replaced in between - a lock changing hands, a
|
|
95
|
+
* record renamed over - then names what was opened, as POSIX's O_NOFOLLOW open
|
|
96
|
+
* would have taken it; a name that moved on again is opened afresh, a few times.
|
|
97
|
+
*/
|
|
98
|
+
export async function openNoFollow(file, flags, { mode, platform = process.platform,
|
|
99
|
+
open = fs.open, lstat = fs.lstat, deadlineAt, sleep } = {}) {
|
|
100
|
+
if (!isWindows(platform)) return open(file, flags | constants.O_NOFOLLOW, mode);
|
|
101
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
102
|
+
const before = await lstat(file, { bigint: true }).catch(error => {
|
|
103
|
+
if (error.code === "ENOENT") return null;
|
|
104
|
+
throw error;
|
|
105
|
+
});
|
|
106
|
+
if (before?.isSymbolicLink()) throw refusedLink(file);
|
|
107
|
+
// A scanner that opened the file without read sharing refuses every reader
|
|
108
|
+
// with EBUSY until it lets go.
|
|
109
|
+
const handle = await retrying(() => open(file, flags, mode), error => error.code === "EBUSY",
|
|
110
|
+
{ deadlineAt, sleep }).catch(async error => {
|
|
111
|
+
// A name another process is deleting, or a lock changing hands around
|
|
112
|
+
// it, refuses the open with EPERM, and the name is gone right after
|
|
113
|
+
// (measured on windows-latest, for a name present before the open and
|
|
114
|
+
// for one absent before it): that is ENOENT, as Linux says it. A name
|
|
115
|
+
// that is still there keeps its EPERM, and so does a create on a name
|
|
116
|
+
// that was free, which is a refused create.
|
|
117
|
+
const refusedCreate = before === null && (flags & constants.O_CREAT) !== 0;
|
|
118
|
+
if (error.code !== "EPERM" || refusedCreate || !await gone(file, lstat)) throw error;
|
|
119
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${file}'`,
|
|
120
|
+
{ cause: error }), { code: "ENOENT", syscall: "open", path: file });
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const opened = await handle.stat({ bigint: true });
|
|
124
|
+
const named = await lstat(file, { bigint: true }).catch(error => {
|
|
125
|
+
if (error.code === "ENOENT") return null;
|
|
126
|
+
throw error;
|
|
127
|
+
});
|
|
128
|
+
if (named?.isSymbolicLink()) throw refusedLink(file);
|
|
129
|
+
if (named !== null && named.dev === opened.dev && named.ino === opened.ino) return handle;
|
|
130
|
+
if (attempt >= SWAPS) throw refusedLink(file);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
await handle.close();
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Make a rename or link that produced `entry` inside `directory` durable.
|
|
141
|
+
*
|
|
142
|
+
* POSIX syncs the directory. Windows refuses a flush on a directory handle, and
|
|
143
|
+
* NTFS journals the rename as metadata: flushing the renamed file commits that
|
|
144
|
+
* journal up to the file's last change, which includes the rename. A directory
|
|
145
|
+
* entry (a lock taken by renaming a directory) guards live processes only and is
|
|
146
|
+
* not flushed.
|
|
147
|
+
*/
|
|
148
|
+
export async function syncEntry(directory, entry, { platform = process.platform,
|
|
149
|
+
open = fs.open, lstat = fs.lstat, deadlineAt, sleep } = {}) {
|
|
150
|
+
if (!isWindows(platform)) {
|
|
151
|
+
const handle = await open(directory, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
152
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if ((await lstat(entry)).isDirectory()) return;
|
|
156
|
+
const handle = await retrying(() => open(entry, "r+"), error => error.code === "EBUSY",
|
|
157
|
+
{ deadlineAt, sleep });
|
|
158
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Remove a tree; Windows retries a file another process still holds. */
|
|
162
|
+
export async function removeTree(target, { platform = process.platform, rm = fs.rm } = {}) {
|
|
163
|
+
await rm(target, { recursive: true, force: true,
|
|
164
|
+
maxRetries: isWindows(platform) ? 10 : 0, retryDelay: 50 });
|
|
165
|
+
}
|
|
@@ -32,7 +32,7 @@ export async function scheduleWorker(root, control, { env = process.env } = {})
|
|
|
32
32
|
}
|
|
33
33
|
const child = spawn(process.execPath,
|
|
34
34
|
[path.join(control.active.root, "bin", "acc-update-worker.mjs"), root], {
|
|
35
|
-
env, detached: true, stdio: "ignore", cwd: root,
|
|
35
|
+
env, detached: true, windowsHide: true, stdio: "ignore", cwd: root,
|
|
36
36
|
});
|
|
37
37
|
child.on("error", () => {});
|
|
38
38
|
child.unref();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { lstat, mkdir, open, realpath,
|
|
3
|
+
import { lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { collectFile, fingerprint } from "./generation-files.mjs";
|
|
6
|
+
import { isWindows, openNoFollow, renameReplacing, syncEntry } from "./portable-fs.mjs";
|
|
6
7
|
|
|
7
8
|
/** Resolve existing ancestors without creating an uninitialized manager. */
|
|
8
9
|
export async function canonicalManagerRoot(root) {
|
|
@@ -26,8 +27,10 @@ export async function managedDirectory(directory, { create = false } = {}) {
|
|
|
26
27
|
try {
|
|
27
28
|
const info = await lstat(directory);
|
|
28
29
|
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("managed directory is not a regular directory");
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// Mode bits are POSIX privacy. Windows refuses fchmod on a directory
|
|
31
|
+
// handle, and privacy there is the profile ACL the directory inherits.
|
|
32
|
+
if (create && !isWindows()) {
|
|
33
|
+
const handle = await openNoFollow(directory, constants.O_RDONLY);
|
|
31
34
|
try { await handle.chmod(0o700); } finally { await handle.close(); }
|
|
32
35
|
}
|
|
33
36
|
return true;
|
|
@@ -37,7 +40,11 @@ export async function managedDirectory(directory, { create = false } = {}) {
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/** Make removals and directory renames in `directory` durable. Windows refuses a
|
|
44
|
+
* flush on a directory handle; a record written by writeManagedJson is flushed
|
|
45
|
+
* itself there, and a removal that a power cut undoes only keeps a hold longer. */
|
|
40
46
|
export async function syncDirectory(directory) {
|
|
47
|
+
if (isWindows()) return;
|
|
41
48
|
const handle = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
42
49
|
try { await handle.sync(); } finally { await handle.close(); }
|
|
43
50
|
}
|
|
@@ -46,7 +53,7 @@ export async function syncDirectory(directory) {
|
|
|
46
53
|
export async function readManagedJson(file) {
|
|
47
54
|
let handle;
|
|
48
55
|
try {
|
|
49
|
-
handle = await
|
|
56
|
+
handle = await openNoFollow(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
50
57
|
if (!(await handle.stat()).isFile()) throw new Error("managed record must be a regular file");
|
|
51
58
|
return JSON.parse(await handle.readFile("utf8"));
|
|
52
59
|
} catch (error) {
|
|
@@ -55,17 +62,20 @@ export async function readManagedJson(file) {
|
|
|
55
62
|
} finally { await handle?.close(); }
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
// `flushName: false` leaves the name to the next flush, for a record whose loss
|
|
66
|
+
// on a crash is recovered by writing it again. The bytes are always flushed.
|
|
67
|
+
export async function writeManagedJson(file, value, { flushName = true } = {}) {
|
|
59
68
|
const temporary = path.join(path.dirname(file), `.record-${randomUUID()}.tmp`);
|
|
60
69
|
let handle;
|
|
61
70
|
try {
|
|
62
|
-
handle = await
|
|
71
|
+
handle = await openNoFollow(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
|
72
|
+
{ mode: 0o600 });
|
|
63
73
|
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
64
74
|
await handle.sync();
|
|
65
75
|
await handle.close();
|
|
66
76
|
handle = null;
|
|
67
|
-
await
|
|
68
|
-
await
|
|
77
|
+
await renameReplacing(temporary, file);
|
|
78
|
+
if (flushName) await syncEntry(path.dirname(file), file);
|
|
69
79
|
} finally {
|
|
70
80
|
await handle?.close();
|
|
71
81
|
await rm(temporary, { force: true });
|
|
@@ -73,7 +73,12 @@ export async function updateNativeSessions(adapters, { service, status, root, no
|
|
|
73
73
|
: binding === undefined ? "unbound"
|
|
74
74
|
: delivery === null ? (leased ? "active" : "degraded")
|
|
75
75
|
: !delivery.deliverable ? "degraded" : leased ? "active" : "idle";
|
|
76
|
+
// A session in another product the adapter serves - Antigravity 2.0
|
|
77
|
+
// beside Antigravity CLI - is named by that product.
|
|
78
|
+
const product = (byAdapterId.get(entry.adapterId)?.client?.variants ?? [])
|
|
79
|
+
.find(variant => variant?.certificationName === owner?.clientName);
|
|
76
80
|
native.sessions.push({ sessionId: peer.sessionId, participantId: peer.participantId,
|
|
81
|
+
...(product === undefined ? {} : { displayName: product.displayName }),
|
|
77
82
|
presence: peer.presence, runtime, clientVersion: binding?.clientVersion ?? null, delivery,
|
|
78
83
|
lastAttempt: owner === null ? null : await loadNativeAttempt({ runtimeDir: root, ...owner }) });
|
|
79
84
|
}
|
|
@@ -104,9 +109,12 @@ const failures = {
|
|
|
104
109
|
session_generation_stale: "the session was replaced during the handshake",
|
|
105
110
|
workspace_identity_unavailable: "the bound client session is no longer in this workspace; "
|
|
106
111
|
+ "start a new client session here",
|
|
107
|
-
// It stays embedded until it closes; a later service never adopts it.
|
|
112
|
+
// It stays embedded until it closes; a later service never adopts it. The
|
|
113
|
+
// Codex app runs every chat this way (openai/codex#41014), so a new chat
|
|
114
|
+
// there is embedded too; one started in a terminal is not.
|
|
108
115
|
client_session_embedded: "the client runs this session on its own embedded service, which "
|
|
109
|
-
+ "peers cannot reach; start a new client session while its local delivery
|
|
116
|
+
+ "peers cannot reach; start a new client session in a terminal while its local delivery "
|
|
117
|
+
+ "service runs (the Codex app runs every chat embedded)",
|
|
110
118
|
};
|
|
111
119
|
|
|
112
120
|
function describeAttempt(attempt) {
|
|
@@ -149,7 +157,7 @@ const RUNTIME_TEXT = { active: "local transport active", idle: "idle, lease laps
|
|
|
149
157
|
|
|
150
158
|
export function nativeSessionLines(adapters) {
|
|
151
159
|
return adapters.flatMap(entry => (entry.nativeDelivery.sessions ?? []).map(session =>
|
|
152
|
-
` ${entry.displayName} session ${session.participantId} (${session.sessionId}): `
|
|
160
|
+
` ${session.displayName ?? entry.displayName} session ${session.participantId} (${session.sessionId}): `
|
|
153
161
|
+ `${RUNTIME_TEXT[session.runtime] ?? session.runtime}; `
|
|
154
162
|
+ [describeDelivery(session), session.runtime === "idle" ? "the next send refreshes it" : null,
|
|
155
163
|
describeAttempt(session.lastAttempt)].filter(part => part !== null).join("; ")));
|