agents-can-communicate 0.10.4 → 0.11.0
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 +29 -21
- package/bin/acc-indicator.mjs +5 -0
- package/docs/CAPABILITIES.md +3 -0
- package/docs/CLI.md +34 -7
- package/docs/MCP.md +42 -2
- package/docs/PROTOCOL.md +19 -0
- package/docs/STATUS_INDICATOR.md +128 -0
- package/docs/WHY_ACC.md +14 -8
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -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 +29 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +67 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
- 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 +29 -5
- 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 +29 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- 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 +29 -5
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator.mjs +23 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +9 -5
- 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 +29 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator.mjs +17 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +12 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +5 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-command.mjs +14 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-text.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-order.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +15 -8
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +3 -53
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +19 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +14 -5
- 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/installer/src/apply.mjs +3 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
- package/package.json +3 -1
|
@@ -33,8 +33,8 @@ const SUMMARY = Object.freeze({
|
|
|
33
33
|
prune: "report what this workspace no longer needs; --apply to reclaim it",
|
|
34
34
|
sync: "events since a cursor, or bounded message history",
|
|
35
35
|
work: "publish what this session is doing, or --clear when it has stopped",
|
|
36
|
-
claim: "reserve
|
|
37
|
-
release: "give
|
|
36
|
+
claim: "reserve resources atomically; exit 5 when a peer holds one",
|
|
37
|
+
release: "give selected claims back, or --all for this session",
|
|
38
38
|
ack: "acknowledge receipt without accepting work or writing an answer",
|
|
39
39
|
message: "send a typed message to named participants",
|
|
40
40
|
inbox: "list pending message headers; --message reads one complete message",
|
|
@@ -72,7 +72,7 @@ const CHOICES = Object.freeze({
|
|
|
72
72
|
finish: { status: HANDOFF_STATUSES },
|
|
73
73
|
message: { type: GENERIC_MESSAGE_KINDS, obligation: OBLIGATIONS },
|
|
74
74
|
sync: { scope: ["delta", "full", "history"], type: MESSAGE_KINDS },
|
|
75
|
-
install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS },
|
|
75
|
+
install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS, indicator: ["on", "off"] },
|
|
76
76
|
uninstall: { adapter: ADAPTER_IDS },
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -80,6 +80,7 @@ const NOTES = Object.freeze({
|
|
|
80
80
|
doctor: ["--repair completes recoverable writes without changing the store contract.",
|
|
81
81
|
"--migrate-store explicitly changes contract 6 to 7 after old clients and unmanaged store users stop; it cannot be combined with --repair."],
|
|
82
82
|
install: ["Without --delivery, one default-No question covers all selected clients that need a decision.",
|
|
83
|
+
"--indicator on|off controls the compact indicator in supported clients; omission preserves the saved choice (initially off).",
|
|
83
84
|
"--delivery actionable|all gives explicit complete-setup consent; --delivery off disables incoming automatic requests.",
|
|
84
85
|
"A dry run does not prompt, write configuration, or start a service. Service readiness does not establish a session binding."],
|
|
85
86
|
attach: ["Native hooks already manage their own session. Attach only for a manual CLI session.",
|
|
@@ -87,8 +88,12 @@ const NOTES = Object.freeze({
|
|
|
87
88
|
"--cadence is a positive interval in milliseconds."],
|
|
88
89
|
work: ["Provide --summary unless using --clear. Default mode: edit; default state: active."],
|
|
89
90
|
claim: ["--lease is in seconds. Default mode: exclusive; default enforcement: advisory.",
|
|
90
|
-
"
|
|
91
|
-
|
|
91
|
+
"Repeat --resource for an atomic batch. A conflict changes no claims, including existing leases.",
|
|
92
|
+
"For files, use file:src/a.mjs or file:src/**. Duplicate canonical resources are rejected."],
|
|
93
|
+
release: ["Choose repeated --claim, repeated --resource, or --all. Never combine selector families.",
|
|
94
|
+
"Explicit lists succeed completely or change no claims. --all releases only this session's claims and keeps it open.",
|
|
95
|
+
"An empty --all succeeds. A repeated explicit release of missing claims remains an error.",
|
|
96
|
+
"Releasing another owner's claim requires human or policy authority and a single selector."],
|
|
92
97
|
message: ["Default type: note. question/request require --to and obligation reply; note uses none.",
|
|
93
98
|
"decision permits none, or acknowledge when addressed. Use reply for answers and finish for handoffs.",
|
|
94
99
|
"For decisions, repeat --supersedes or --withdraws with 1..16 prior decision IDs; never combine the two.",
|
|
@@ -148,11 +153,13 @@ export function commandHelpText(command) {
|
|
|
148
153
|
const lines = [`acc ${name} - ${summary}`, "", `Usage: acc ${name}${suffix} [options]`, ""];
|
|
149
154
|
for (const [key, label] of [["required", "Required"], ["optional", "Optional"],
|
|
150
155
|
["repeated", "Repeatable"], ["flags", "Flags"]]) {
|
|
151
|
-
|
|
156
|
+
const options = command[key].filter(option => key !== "repeated" || !command.required.includes(option));
|
|
157
|
+
if (options.length === 0) continue;
|
|
152
158
|
lines.push(`${label}:`);
|
|
153
|
-
for (const option of
|
|
159
|
+
for (const option of options) {
|
|
154
160
|
const value = key === "flags" ? "" : ` <${choices[option]?.join("|") ?? "value"}>`;
|
|
155
|
-
|
|
161
|
+
const repeatable = key === "required" && command.repeated.includes(option) ? " (repeatable)" : "";
|
|
162
|
+
lines.push(` --${option}${value}${repeatable}`);
|
|
156
163
|
}
|
|
157
164
|
lines.push("");
|
|
158
165
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Composition root: discovery, runtime locations, and the CLI surface.
|
|
2
2
|
export { main } from "./main.mjs";
|
|
3
|
+
export { readIndicator } from "./indicator.mjs";
|
|
3
4
|
export { COMMANDS, parseArgs } from "./args.mjs";
|
|
4
5
|
export { askConfirmation } from "./confirm.mjs";
|
|
5
6
|
// Exported so a test can hold every adapter to where it plans to write and
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readTomlIndicatorSettings } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { previousOutput } from "./indicator-previous-command.mjs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { readIndicator, indicatorFailure } from "./indicator.mjs";
|
|
5
|
+
import { ALL_ADAPTERS } from "./install-command.mjs";
|
|
6
|
+
|
|
7
|
+
function output(report, options, context) {
|
|
8
|
+
if (options.json) return JSON.stringify(report);
|
|
9
|
+
if (options.details) return [report.label, report.detail, report.action].filter(Boolean).join("\n");
|
|
10
|
+
const extension = context.extension;
|
|
11
|
+
let label = report.label;
|
|
12
|
+
try { label = extension?.renderText?.(report) ?? label; }
|
|
13
|
+
catch { /* A display extension cannot prevent the diagnostic. */ }
|
|
14
|
+
const indicator = label + (report.health === "problem" ? " · acc doctor" : "");
|
|
15
|
+
try {
|
|
16
|
+
if (extension?.composeText) return extension.composeText({ ...context, indicator });
|
|
17
|
+
} catch { /* Keep the diagnostic if the footer cannot be composed. */ }
|
|
18
|
+
return [context.previous, indicator].filter(Boolean).join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function input(stream) {
|
|
22
|
+
let source = "";
|
|
23
|
+
for await (const chunk of stream) {
|
|
24
|
+
source += chunk;
|
|
25
|
+
if (Buffer.byteLength(source) > 64 * 1024) throw new Error("indicator input too large");
|
|
26
|
+
}
|
|
27
|
+
return source;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runIndicator({ argv = process.argv.slice(2), stdin = process.stdin,
|
|
31
|
+
stdout = process.stdout } = {}) {
|
|
32
|
+
const options = {}, context = { payload: {}, previous: "", settings: {} };
|
|
33
|
+
let report = indicatorFailure(), timeout, expired = false;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
try {
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const flag = argv[i];
|
|
38
|
+
if (["--json", "--details"].includes(flag)) options[flag.slice(2)] = true;
|
|
39
|
+
else if (["--adapter", "--native-session", "--previous"].includes(flag)
|
|
40
|
+
&& typeof argv[i + 1] === "string") options[flag.slice(2)] = argv[++i];
|
|
41
|
+
else throw new Error("invalid indicator option");
|
|
42
|
+
}
|
|
43
|
+
context.extension = ALL_ADAPTERS().find(adapter => adapter.id === options.adapter)?.statusIndicator;
|
|
44
|
+
const plain = options.json || options.details;
|
|
45
|
+
timeout = setTimeout(() => {
|
|
46
|
+
expired = true;
|
|
47
|
+
controller.abort();
|
|
48
|
+
stdout.write(output(indicatorFailure(), options, context) + "\n", () => process.exit(0));
|
|
49
|
+
}, plain ? 1500 : context.extension?.budgetMs ?? 1500);
|
|
50
|
+
const source = options["native-session"] ? "{}" : await input(stdin);
|
|
51
|
+
context.payload = source.trim() ? JSON.parse(source) : {};
|
|
52
|
+
report = await readIndicator({ adapterId: options.adapter,
|
|
53
|
+
nativeSessionId: options["native-session"] ?? (context.extension?.nativeSessionId
|
|
54
|
+
? context.extension.nativeSessionId(context.payload)
|
|
55
|
+
: context.payload.conversation_id ?? context.payload.session_id) });
|
|
56
|
+
if (options.previous && !plain) {
|
|
57
|
+
const saved = JSON.parse(await readFile(options.previous, "utf8"));
|
|
58
|
+
context.settings = await readTomlIndicatorSettings(saved);
|
|
59
|
+
context.previous = await previousOutput(saved.previous, source,
|
|
60
|
+
context.extension?.previousTimeoutMs ?? 700, controller.signal);
|
|
61
|
+
}
|
|
62
|
+
} catch { /* A status renderer fails open with an actionable diagnostic. */ }
|
|
63
|
+
finally { clearTimeout(timeout); controller.abort(); }
|
|
64
|
+
if (!expired) stdout.write(output(report, options, context) + "\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const LIMIT = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
function stopTree(child) {
|
|
8
|
+
if (!Number.isInteger(child?.pid) || child.pid <= 0) return;
|
|
9
|
+
if (process.platform !== "win32") {
|
|
10
|
+
// detached creates a new group for this command, never for the host client.
|
|
11
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { /* Already gone. */ }
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Keep the owned launcher alive until taskkill discovers its descendants.
|
|
15
|
+
// Killing it first loses that relationship; an exited PID must never be reused.
|
|
16
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
17
|
+
const root = process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "C:\\Windows";
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
// Launcher and cleanup must survive the outer deadline: libuv kills
|
|
20
|
+
// non-detached Windows children when their spawning Node process exits.
|
|
21
|
+
const killer = spawn(path.win32.join(root, "System32", "taskkill.exe"),
|
|
22
|
+
["/pid", String(child.pid), "/T", "/F"], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
23
|
+
const timer = setTimeout(() => { killer.kill(); resolve(); }, 1000);
|
|
24
|
+
const done = () => { clearTimeout(timer); resolve(); };
|
|
25
|
+
killer.once("error", done);
|
|
26
|
+
killer.once("exit", done);
|
|
27
|
+
killer.unref();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The original command receives the original stdin. Cancellation belongs to
|
|
32
|
+
// the indicator request; its timeout must not leave another polling process.
|
|
33
|
+
export async function previousOutput(previous, payload, timeout, signal) {
|
|
34
|
+
if (signal?.aborted || previous?.enabled === false
|
|
35
|
+
|| ["builtin", "disabled", "off", "none", "hidden"].includes(previous?.type)
|
|
36
|
+
|| typeof previous?.command !== "string" || !previous.command) return "";
|
|
37
|
+
return new Promise(resolve => {
|
|
38
|
+
let child, timer, settled = false, text = "";
|
|
39
|
+
const finish = () => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
signal?.removeEventListener("abort", finish);
|
|
44
|
+
// Continue draining stdout until Windows has stopped the tree. Closing
|
|
45
|
+
// it first can make the shell exit before taskkill inspects its children.
|
|
46
|
+
let cleanup;
|
|
47
|
+
try { cleanup = stopTree(child); } catch { /* Cleanup must fail open. */ }
|
|
48
|
+
Promise.resolve(cleanup).catch(() => {}).finally(() => {
|
|
49
|
+
child?.stdin?.destroy();
|
|
50
|
+
child?.stdout?.destroy();
|
|
51
|
+
if (child?.connected) child.disconnect();
|
|
52
|
+
child?.unref();
|
|
53
|
+
resolve(text.slice(0, LIMIT).trimEnd());
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const windows = process.platform === "win32";
|
|
58
|
+
child = windows
|
|
59
|
+
? spawn(process.execPath, [fileURLToPath(new URL("./indicator-previous-windows.mjs", import.meta.url)), previous.command],
|
|
60
|
+
{ detached: true, stdio: ["pipe", "pipe", "ignore", "ipc"], windowsHide: true })
|
|
61
|
+
: spawn(previous.command, { shell: true, detached: true,
|
|
62
|
+
stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
|
|
63
|
+
child.stdout.on("data", chunk => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
text += chunk;
|
|
66
|
+
if (Buffer.byteLength(text) > LIMIT) finish();
|
|
67
|
+
});
|
|
68
|
+
child.stdin.on("error", () => {});
|
|
69
|
+
child.once("error", finish);
|
|
70
|
+
child.once("close", finish);
|
|
71
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
72
|
+
// The command budget starts when the Windows launcher starts the shell.
|
|
73
|
+
// Startup is bounded too; the enclosing indicator deadline can abort both.
|
|
74
|
+
timer = setTimeout(finish, windows ? 1000 : timeout);
|
|
75
|
+
if (windows) child.once("message", message => {
|
|
76
|
+
if (settled || message !== "started") return;
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
timer = setTimeout(finish, timeout);
|
|
79
|
+
});
|
|
80
|
+
child.stdin.end(payload);
|
|
81
|
+
if (signal?.aborted) finish();
|
|
82
|
+
} catch { finish(); }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// A detached cmd.exe loses its standard handles. Keep the owned Node launcher
|
|
4
|
+
// detached, and let a normal hidden shell inherit its original stdin/stdout.
|
|
5
|
+
const child = spawn(process.argv[2], { shell: true, windowsHide: true, stdio: "inherit" });
|
|
6
|
+
child.once("spawn", () => process.send?.("started", () => {}));
|
|
7
|
+
child.once("error", () => process.exit(0));
|
|
8
|
+
child.once("exit", () => process.exit(0));
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
4
|
+
import { effectiveCapabilities, loadNativeAttempt, loadSessionBinding, loadOfferObservation }
|
|
5
|
+
from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
import { readDeliveryBindingRecord, readSessionRecord } from "@agents-can-communicate/storage-filesystem";
|
|
7
|
+
import { readInstalledLivePolicyState } from "@agents-can-communicate/installer";
|
|
8
|
+
import { ALL_ADAPTERS } from "./install-command.mjs";
|
|
9
|
+
import { canonicalManagerRoot, managedDirectory, readManagedJson } from "./managed-runtime/state.mjs";
|
|
10
|
+
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
11
|
+
|
|
12
|
+
const receptionText = {
|
|
13
|
+
automatic: "This chat can receive addressed requests without another user prompt. A busy client may queue them.",
|
|
14
|
+
turn: "Messages reach this chat on its next normal turn.",
|
|
15
|
+
inbox: "Messages are available when the agent reads acc inbox; no polling schedule is implied.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function result({ health = "ready", reception = null, reasonCode = null,
|
|
19
|
+
detail, action = null, leaseCurrent = null }) {
|
|
20
|
+
const qualifier = reception && reception !== "automatic" ? ` · ${reception}` : "";
|
|
21
|
+
return { health, reception, reasonCode, leaseCurrent,
|
|
22
|
+
label: `ACC ${health === "ready" ? "●" : "!"}${qualifier}`,
|
|
23
|
+
detail: [detail, receptionText[reception]].filter(Boolean).join(" "), action };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const indicatorFailure = (reasonCode = "state_unreadable") => result({
|
|
27
|
+
health: "problem", reasonCode,
|
|
28
|
+
detail: reasonCode === "not_registered" ? "This chat is not registered with ACC."
|
|
29
|
+
: "ACC could not read this chat's local integration state.",
|
|
30
|
+
action: reasonCode === "not_registered"
|
|
31
|
+
? "Send a prompt to run the session hooks. If ACC is still absent, run acc doctor and check the plugin is enabled."
|
|
32
|
+
: "Run acc doctor in this project to identify and repair the local state problem.",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const actions = {
|
|
36
|
+
client_session_embedded: "This session has no shared delivery service. Run acc doctor to see the launch option and how to open a supported session.",
|
|
37
|
+
client_process_unknown: "Open a new client session so ACC can identify its process; then run acc doctor.",
|
|
38
|
+
transport_permission_denied: "Allow the sending command to access the local delivery channel; acc doctor shows the affected session.",
|
|
39
|
+
handshake_version_mismatch: "Update the client's delivery service to a supported version; acc doctor shows the version in use.",
|
|
40
|
+
native_policy_off: "Automatic delivery is disabled in ACC settings. Run acc install --delivery actionable to enable it.",
|
|
41
|
+
inbound_approval_required: "Approve the pending inbound request in the receiving client, or review its incoming-message setting.",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A display observation, never CLI ownership. Native ids select one saved room
|
|
45
|
+
* and one binding; there is no roster scan, Git probe, store open, socket probe,
|
|
46
|
+
* lease refresh, heartbeat, update worker or managed-runtime process lease. */
|
|
47
|
+
export async function readIndicator({ adapterId, nativeSessionId,
|
|
48
|
+
dataHome = platformDataHome(), now = new Date().toISOString() }) {
|
|
49
|
+
try {
|
|
50
|
+
const adapter = ALL_ADAPTERS().find(item => item.id === adapterId);
|
|
51
|
+
if (!adapter || typeof nativeSessionId !== "string" || !nativeSessionId
|
|
52
|
+
|| nativeSessionId.length > 4096) return indicatorFailure("not_registered");
|
|
53
|
+
const key = createHash("sha256").update(JSON.stringify([adapterId, nativeSessionId])).digest("hex");
|
|
54
|
+
const accRoot = await canonicalManagerRoot(path.join(dataHome, "acc"));
|
|
55
|
+
const rooms = path.join(accRoot, "native-workspaces");
|
|
56
|
+
if (!await managedDirectory(rooms)) return indicatorFailure("not_registered");
|
|
57
|
+
const room = await readManagedJson(path.join(rooms, `${key}.json`));
|
|
58
|
+
if (!room) return indicatorFailure("not_registered");
|
|
59
|
+
if (room.schemaVersion !== 1 || room.adapterId !== adapterId
|
|
60
|
+
|| room.nativeSessionId !== nativeSessionId) throw new Error("invalid room");
|
|
61
|
+
assertPortableId(room.workspaceId, "workspace id");
|
|
62
|
+
const root = runtimePaths({ dataHome: path.dirname(accRoot), workspaceId: room.workspaceId }).root;
|
|
63
|
+
const owner = await loadSessionBinding({ runtimeDir: root, harnessSessionId: nativeSessionId });
|
|
64
|
+
if (!owner) return indicatorFailure("not_registered");
|
|
65
|
+
const input = { root, workspaceId: room.workspaceId, sessionId: owner.accSessionId };
|
|
66
|
+
const session = await readSessionRecord(input);
|
|
67
|
+
if (session?.state !== "open" || session.generation !== owner.generation
|
|
68
|
+
|| session.harness !== adapterId) return indicatorFailure("not_registered");
|
|
69
|
+
const [binding, handshake, policy, offer] = await Promise.all([
|
|
70
|
+
readDeliveryBindingRecord(input),
|
|
71
|
+
loadNativeAttempt({ runtimeDir: root, harnessSessionId: nativeSessionId, ...owner }),
|
|
72
|
+
readInstalledLivePolicyState({ dataHome: path.dirname(accRoot), adapterId }),
|
|
73
|
+
loadOfferObservation({ runtimeDir: root, sessionId: owner.accSessionId, generation: owner.generation }),
|
|
74
|
+
]);
|
|
75
|
+
const attempt = offer && (!handshake || Date.parse(offer.at) >= Date.parse(handshake.at)) ? offer : handshake;
|
|
76
|
+
const current = await readSessionRecord(input);
|
|
77
|
+
if (current?.state !== "open" || current.generation !== owner.generation) {
|
|
78
|
+
return indicatorFailure("not_registered");
|
|
79
|
+
}
|
|
80
|
+
const capabilities = effectiveCapabilities(adapter, {
|
|
81
|
+
clientName: owner.clientName ?? adapter.client.certificationName,
|
|
82
|
+
clientVersion: owner.clientVersion, platform: owner.platform,
|
|
83
|
+
});
|
|
84
|
+
const fallback = capabilities.delivery.nextTurn ? "turn" : "inbox";
|
|
85
|
+
const bound = binding?.generation === owner.generation && binding.adapterId === adapterId
|
|
86
|
+
&& binding.retiredAt == null && binding.availableModes.includes("livePush");
|
|
87
|
+
const leaseCurrent = bound ? Date.parse(binding.leaseUntil) > Date.parse(now) : null;
|
|
88
|
+
if (["missing", "invalid", "unavailable"].includes(policy.policyStatus)) {
|
|
89
|
+
return result({ health: "problem", reception: fallback, reasonCode: "installation_unavailable",
|
|
90
|
+
detail: "ACC cannot find a valid delivery setting for this integration.",
|
|
91
|
+
action: `Run acc install --adapter ${adapterId} to repair the integration.`, leaseCurrent });
|
|
92
|
+
}
|
|
93
|
+
if (policy.policy === "off") return result({ reception: fallback, reasonCode: "native_policy_off",
|
|
94
|
+
detail: "Automatic delivery is disabled in ACC settings.", leaseCurrent });
|
|
95
|
+
if (attempt?.state === "degraded") return result({ health: "problem", reception: fallback,
|
|
96
|
+
reasonCode: attempt.reasonCode ?? "handshake_failed", leaseCurrent,
|
|
97
|
+
detail: "ACC could not establish automatic delivery for this chat.",
|
|
98
|
+
action: actions[attempt.reasonCode]
|
|
99
|
+
?? "Run acc doctor in this project for the channel failure and the client's recovery steps.",
|
|
100
|
+
});
|
|
101
|
+
// Lease expiry alone is neither disconnection nor an operation in progress.
|
|
102
|
+
// The delivery router verifies the endpoint and renews it on the next send.
|
|
103
|
+
if (bound && binding.livePolicy !== "off") return result({ reception: "automatic", leaseCurrent });
|
|
104
|
+
if (attempt?.state === "unsupported" || !capabilities.delivery.livePush) {
|
|
105
|
+
return result({ reception: fallback, reasonCode: "native_delivery_unsupported",
|
|
106
|
+
detail: "This client session does not support automatic wake-up." });
|
|
107
|
+
}
|
|
108
|
+
return result({ health: "problem", reception: fallback, reasonCode: "native_unbound",
|
|
109
|
+
detail: "This chat has no automatic delivery channel.",
|
|
110
|
+
action: "Send a prompt to let ACC establish the channel; if it remains unavailable, run acc doctor." });
|
|
111
|
+
} catch { return indicatorFailure(); }
|
|
112
|
+
}
|
|
@@ -224,6 +224,9 @@ export function actedOn(result) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
227
|
+
if (options.indicator !== undefined && !["on", "off"].includes(options.indicator)) {
|
|
228
|
+
throw new AccError(EXIT.USAGE, "--indicator must be on or off");
|
|
229
|
+
}
|
|
227
230
|
const adapters = selectAdapters(options.adapter);
|
|
228
231
|
const home = options.home ?? runtime.env?.HOME ?? homedir();
|
|
229
232
|
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
@@ -261,6 +264,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
261
264
|
? await decideDelivery({ options, detected, recorded, runtime, dryRun })
|
|
262
265
|
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
263
266
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
267
|
+
indicator: options.indicator,
|
|
264
268
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
265
269
|
deliveryByAdapter: decided.deliveryByAdapter,
|
|
266
270
|
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
|
|
@@ -23,7 +23,7 @@ import { runDoctor } from "./doctor-command.mjs";
|
|
|
23
23
|
import { runManagementDoctor } from "./managed-runtime/diagnostics.mjs";
|
|
24
24
|
import { runStoreMigration } from "./store-migration-command.mjs";
|
|
25
25
|
import { createGitProbe } from "./git-probe.mjs";
|
|
26
|
-
import {
|
|
26
|
+
import { runClaimCommand, runReleaseCommand } from "./claim-command.mjs";
|
|
27
27
|
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
28
28
|
import { resolveOwner } from "./session-owner.mjs";
|
|
29
29
|
import { discoverWorkspace } from "./workspace-discovery.mjs";
|
|
@@ -31,39 +31,6 @@ import { resolveSavedHookWorkspace } from "./hook-workspace.mjs";
|
|
|
31
31
|
|
|
32
32
|
const DEFAULT_CADENCE_MS = 30_000;
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* The claim a caller means when they name a resource instead of an id.
|
|
36
|
-
*
|
|
37
|
-
* `acc claim` takes `--resource`; `acc release` took `--claim`. The asymmetry
|
|
38
|
-
* cost every caller - a person and an agent alike - a round trip through
|
|
39
|
-
* `acc status --json` to find an id they never chose. The id is still accepted,
|
|
40
|
-
* and is still the precise answer when two sessions hold the same resource and
|
|
41
|
-
* an authority is releasing someone else's.
|
|
42
|
-
*/
|
|
43
|
-
async function claimOn(options, context) {
|
|
44
|
-
if (options.resource === undefined) {
|
|
45
|
-
throw new AccError(EXIT.USAGE,
|
|
46
|
-
"release needs the claim to give back: --resource is what you claimed, "
|
|
47
|
-
+ "--claim is its id");
|
|
48
|
-
}
|
|
49
|
-
const resource = await canonicalClaim(options.resource, context.descriptor);
|
|
50
|
-
const { claims } = await context.service.collectStatus({});
|
|
51
|
-
// An authority releasing another session's claim is not looking for its own.
|
|
52
|
-
const mine = claims.filter(claim => claim.resource === resource
|
|
53
|
-
&& (options.authority !== undefined || claim.ownerSessionId === options.session));
|
|
54
|
-
|
|
55
|
-
if (mine.length === 0) {
|
|
56
|
-
throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
|
|
57
|
-
{ resource, held: claims.map(claim => claim.resource) });
|
|
58
|
-
}
|
|
59
|
-
if (mine.length > 1) {
|
|
60
|
-
throw new AccError(EXIT.DATA,
|
|
61
|
-
`${mine.length} claims on ${resource}; name the one you mean with --claim`,
|
|
62
|
-
{ resource, claims: mine.map(claim => claim.claimId) });
|
|
63
|
-
}
|
|
64
|
-
return mine[0].claimId;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
34
|
/**
|
|
68
35
|
* Where this workspace's state lives, without reading a byte of it.
|
|
69
36
|
*
|
|
@@ -327,25 +294,8 @@ const HANDLERS = Object.freeze({
|
|
|
327
294
|
return { data: intent, text: `intent: ${intent.summary}` };
|
|
328
295
|
},
|
|
329
296
|
|
|
330
|
-
claim:
|
|
331
|
-
|
|
332
|
-
generation: options.generation,
|
|
333
|
-
resource: await canonicalClaim(options.resource, context.descriptor),
|
|
334
|
-
mode: options.mode ?? "exclusive", enforcement: options.enforcement ?? "advisory",
|
|
335
|
-
reason: options.reason ?? "unspecified",
|
|
336
|
-
leaseSeconds: options.lease ? positiveNumber(options.lease, "lease") : undefined,
|
|
337
|
-
descriptor: context.descriptor });
|
|
338
|
-
return { data: claim, text: `claimed ${claim.resource}` };
|
|
339
|
-
},
|
|
340
|
-
|
|
341
|
-
release: async ({ options, context }) => {
|
|
342
|
-
const claimId = options.claim ?? await claimOn(options, context);
|
|
343
|
-
const request = { claimId, sessionId: options.session,
|
|
344
|
-
generation: options.generation, authority: options.authority, reason: options.reason };
|
|
345
|
-
if (options.authority === undefined) await context.service.releaseClaim(request);
|
|
346
|
-
else await context.service.forceReleaseClaim(request);
|
|
347
|
-
return { data: { claimId }, text: `released ${claimId}` };
|
|
348
|
-
},
|
|
297
|
+
claim: runClaimCommand,
|
|
298
|
+
release: runReleaseCommand,
|
|
349
299
|
|
|
350
300
|
message: async ({ options, context }) => {
|
|
351
301
|
const kind = options.type ?? "note";
|
|
@@ -32,6 +32,25 @@ export function invokedDirectly(url) {
|
|
|
32
32
|
catch { return false; }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** The display is an observation, not an admitted runtime user. A stable
|
|
36
|
+
* launcher resolves the active generation on every refresh, so an old plugin
|
|
37
|
+
* never retains a path that a same-contract update may reclaim. A concurrent
|
|
38
|
+
* replacement fails open and the next display refresh tries the new pointer. */
|
|
39
|
+
export async function runIndicatorEntry({ managerRoot }) {
|
|
40
|
+
try {
|
|
41
|
+
const root = await canonicalManagerRoot(managerRoot);
|
|
42
|
+
const control = await readControl(root);
|
|
43
|
+
if (!control) throw new Error("indicator runtime missing");
|
|
44
|
+
await import(pathToFileURL(path.join(control.active.root, "bin", "acc-indicator.mjs")).href);
|
|
45
|
+
} catch {
|
|
46
|
+
const report = { health: "problem", reception: null, reasonCode: "indicator_unavailable",
|
|
47
|
+
label: "ACC !", detail: "ACC's installed indicator is unavailable.",
|
|
48
|
+
action: "Run acc update to finish runtime activation, or acc install --indicator on to repair it." };
|
|
49
|
+
process.stdout.write(process.argv.includes("--json") ? JSON.stringify(report) + "\n"
|
|
50
|
+
: "ACC ! · acc doctor\n");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
function unavailable(kind) {
|
|
36
55
|
// Never reflect control file content or payloads into a client diagnostic.
|
|
37
56
|
if (kind === "acc-hook" || kind === "acc-antigravity-relay") {
|
|
@@ -16,6 +16,7 @@ async function durableFile(file, bytes, mode = 0o600) {
|
|
|
16
16
|
export const stablePaths = root => ({
|
|
17
17
|
cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
|
|
18
18
|
antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
|
|
19
|
+
indicatorRunner: path.join(root, "bin", "acc-indicator.mjs"),
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
/** Publish immutable launcher modules, then atomically replace each tiny entry.
|
|
@@ -47,9 +48,10 @@ export async function writeLaunchers(root, packageRoot) {
|
|
|
47
48
|
} finally { await removeTree(staging); }
|
|
48
49
|
const bin = path.join(root, "bin");
|
|
49
50
|
await managedDirectory(bin, { create: true });
|
|
50
|
-
for (const kind of ENTRY_KINDS) {
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
for (const kind of [...ENTRY_KINDS, "acc-indicator"]) {
|
|
52
|
+
const entry = kind === "acc-indicator" ? "runIndicatorEntry" : "runEntry";
|
|
53
|
+
const bytes = `#!/usr/bin/env node\nimport { ${entry} } from "../launchers/${id}/entry.mjs";\n`
|
|
54
|
+
+ `await ${entry}(${JSON.stringify({ kind, packageRoot, managerRoot: root, managedRequired: true })});\n`;
|
|
53
55
|
const temporary = path.join(bin, `.${randomUUID()}.tmp`);
|
|
54
56
|
try {
|
|
55
57
|
await durableFile(temporary, bytes, 0o700);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AccError, EXIT, assertMatchableResource, assertPortableId,
|
|
2
|
+
assertResourceUri, normaliseResource } from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
const usage = message => { throw new AccError(EXIT.USAGE, message); };
|
|
5
|
+
|
|
6
|
+
export function uniqueList(value, name, normalise = item => item) {
|
|
7
|
+
if (!Array.isArray(value) || value.length === 0) usage(`${name} must be a nonempty array`);
|
|
8
|
+
const result = value.map(item => {
|
|
9
|
+
if (typeof item !== "string" || item.length === 0) usage(`${name} must contain nonempty strings`);
|
|
10
|
+
return normalise(item);
|
|
11
|
+
});
|
|
12
|
+
if (new Set(result).size !== result.length) usage(`${name} contains duplicate selectors`);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function claimResources(value) {
|
|
17
|
+
return uniqueList(value, "resources", resource => {
|
|
18
|
+
assertMatchableResource(resource, usage);
|
|
19
|
+
const normalised = normaliseResource(resource);
|
|
20
|
+
try { return assertResourceUri(normalised, "resource"); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!(error instanceof AccError)) throw error;
|
|
23
|
+
throw new AccError(EXIT.USAGE, error.message, error.details);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function releaseSelectors(input) {
|
|
29
|
+
const present = ["claimIds", "resources", "all"].filter(key => input[key] !== undefined);
|
|
30
|
+
if (present.length !== 1 || (present[0] === "all" && input.all !== true)) {
|
|
31
|
+
usage("release needs exactly one of claimIds, resources, or all: true");
|
|
32
|
+
}
|
|
33
|
+
if (input.authority !== undefined) usage("batch release does not accept force-release authority");
|
|
34
|
+
if (input.all === true) return { all: true };
|
|
35
|
+
if (input.resources !== undefined) return { resources: claimResources(input.resources) };
|
|
36
|
+
return { claimIds: uniqueList(input.claimIds, "claimIds", id => {
|
|
37
|
+
try { return assertPortableId(id, "claim id"); }
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!(error instanceof AccError)) throw error;
|
|
40
|
+
throw new AccError(EXIT.USAGE, error.message, error.details);
|
|
41
|
+
}
|
|
42
|
+
}) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function selectReleaseClaims(tx, selectors, sessionId) {
|
|
46
|
+
if (selectors.all === true) return tx.list("claim", claim => claim.ownerSessionId === sessionId);
|
|
47
|
+
if (selectors.claimIds !== undefined) return selectors.claimIds.map(claimId => {
|
|
48
|
+
const claim = tx.get("claim", claimId);
|
|
49
|
+
if (claim === null) throw new AccError(EXIT.CONFLICT, "the claim does not exist", { claimId });
|
|
50
|
+
return claim;
|
|
51
|
+
});
|
|
52
|
+
const claims = tx.list("claim");
|
|
53
|
+
return selectors.resources.map(resource => {
|
|
54
|
+
const mine = claims.filter(claim => claim.resource === resource && claim.ownerSessionId === sessionId);
|
|
55
|
+
if (mine.length === 0) throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
|
|
56
|
+
{ resource, held: claims.map(claim => claim.resource) });
|
|
57
|
+
if (mine.length > 1) throw new AccError(EXIT.DATA,
|
|
58
|
+
`${mine.length} claims on ${resource}; name the one you mean with --claim`,
|
|
59
|
+
{ resource, claims: mine.map(claim => claim.claimId) });
|
|
60
|
+
return mine[0];
|
|
61
|
+
});
|
|
62
|
+
}
|