agents-can-communicate 0.3.0 → 0.4.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 +112 -91
- package/SECURITY.md +8 -5
- package/bin/acc-bootstrap.mjs +5 -54
- package/bin/acc-claude-channel.mjs +8 -175
- package/bin/acc-hook.mjs +4 -133
- package/bin/acc-mcp.mjs +4 -59
- package/bin/acc-update-worker.mjs +8 -0
- package/bin/acc.mjs +5 -39
- package/bin/entrypoints/acc-bootstrap.mjs +47 -0
- package/bin/entrypoints/acc-claude-channel.mjs +149 -0
- package/bin/entrypoints/acc-hook.mjs +58 -0
- package/bin/entrypoints/acc-mcp.mjs +70 -0
- package/bin/entrypoints/acc.mjs +51 -0
- package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
- package/bin/entrypoints/hook-output.mjs +73 -0
- package/docs/ADAPTER_AUTHORING.md +62 -24
- package/docs/ARCHITECTURE.md +79 -20
- package/docs/CAPABILITIES.md +52 -30
- package/docs/CLI.md +164 -35
- package/docs/CONCEPTS.md +97 -93
- package/docs/CONFIGURATION.md +66 -45
- package/docs/DESIGN_DECISIONS.md +24 -15
- package/docs/GETTING_STARTED.md +124 -118
- package/docs/GLOSSARY.md +12 -6
- package/docs/HOW_IT_WORKS.md +136 -229
- package/docs/MCP.md +98 -31
- package/docs/PROTOCOL.md +76 -14
- package/docs/RELEASING.md +128 -86
- package/docs/SECURITY_MODEL.md +52 -15
- package/docs/TROUBLESHOOTING.md +88 -18
- package/docs/UPGRADING.md +119 -0
- package/docs/WHY_ACC.md +19 -16
- package/docs/index.md +40 -24
- 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 +112 -9
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +111 -8
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
- package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -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 +111 -8
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
- 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 +111 -8
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
- 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 +111 -8
- package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
- package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/package.json +4 -2
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
- package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +66 -18
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +48 -24
- package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
- package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
- package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
- package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
- package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
- package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
- package/node_modules/@agents-can-communicate/installer/package.json +4 -2
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
- package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
- package/package.json +2 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
/** Resolve existing ancestors without creating an uninitialized manager. */
|
|
7
|
+
export async function canonicalManagerRoot(root) {
|
|
8
|
+
if (typeof root !== "string" || !path.isAbsolute(root)) throw new Error("manager root must be absolute");
|
|
9
|
+
let current = path.resolve(root);
|
|
10
|
+
const tail = [];
|
|
11
|
+
for (;;) {
|
|
12
|
+
try { return path.join(await realpath(current), ...tail); }
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== "ENOENT") throw error;
|
|
15
|
+
const parent = path.dirname(current);
|
|
16
|
+
if (parent === current) throw error;
|
|
17
|
+
tail.unshift(path.basename(current));
|
|
18
|
+
current = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function managedDirectory(directory, { create = false } = {}) {
|
|
24
|
+
if (create) await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
try {
|
|
26
|
+
const info = await lstat(directory);
|
|
27
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("managed directory is not a regular directory");
|
|
28
|
+
if (create) {
|
|
29
|
+
const handle = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
30
|
+
try { await handle.chmod(0o700); } finally { await handle.close(); }
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (!create && error.code === "ENOENT") return false;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function syncDirectory(directory) {
|
|
40
|
+
const handle = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
41
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Internal callers validate every managed parent before opening a record. */
|
|
45
|
+
export async function readManagedJson(file) {
|
|
46
|
+
let handle;
|
|
47
|
+
try {
|
|
48
|
+
handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
49
|
+
if (!(await handle.stat()).isFile()) throw new Error("managed record must be a regular file");
|
|
50
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === "ENOENT") return undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
} finally { await handle?.close(); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function writeManagedJson(file, value) {
|
|
58
|
+
const temporary = path.join(path.dirname(file), `.record-${randomUUID()}.tmp`);
|
|
59
|
+
let handle;
|
|
60
|
+
try {
|
|
61
|
+
handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
62
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
63
|
+
await handle.sync();
|
|
64
|
+
await handle.close();
|
|
65
|
+
handle = null;
|
|
66
|
+
await rename(temporary, file);
|
|
67
|
+
await syncDirectory(path.dirname(file));
|
|
68
|
+
} finally {
|
|
69
|
+
await handle?.close();
|
|
70
|
+
await rm(temporary, { force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nonempty = value => typeof value === "string" && value.length > 0;
|
|
75
|
+
const nullableString = value => value === null || nonempty(value);
|
|
76
|
+
|
|
77
|
+
export async function validateRuntime(root, runtime) {
|
|
78
|
+
if (!runtime || !nonempty(runtime.version) || typeof runtime.root !== "string"
|
|
79
|
+
|| !path.isAbsolute(runtime.root)) throw new Error("invalid control generation");
|
|
80
|
+
const generations = path.join(root, "generations");
|
|
81
|
+
const resolved = await canonicalManagerRoot(runtime.root);
|
|
82
|
+
const relative = path.relative(generations, resolved);
|
|
83
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
84
|
+
throw new Error("control runtime must be inside manager generations");
|
|
85
|
+
}
|
|
86
|
+
// An internal symlink can redirect all generations outside the manager even
|
|
87
|
+
// when a lexical pointer appears contained.
|
|
88
|
+
if (await managedDirectory(generations)) {
|
|
89
|
+
if (await realpath(generations) !== generations) throw new Error("invalid control generations directory");
|
|
90
|
+
}
|
|
91
|
+
return { version: runtime.version, root: resolved };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function validateControl(root, value) {
|
|
95
|
+
if (!value || value.schemaVersion !== 1 || !["ready", "activating"].includes(value.phase)
|
|
96
|
+
|| typeof value.auto !== "boolean" || !nullableString(value.pin)
|
|
97
|
+
|| !nullableString(value.checkedAt) || !nullableString(value.notice)
|
|
98
|
+
|| !nonempty(value.home) || !path.isAbsolute(value.home)
|
|
99
|
+
|| !Array.isArray(value.targets) || !value.targets.every(nonempty)
|
|
100
|
+
|| !(value.pending === null || typeof value.pending === "object")) {
|
|
101
|
+
throw new Error("invalid managed runtime control");
|
|
102
|
+
}
|
|
103
|
+
const active = await validateRuntime(root, value.active);
|
|
104
|
+
const pending = value.pending === null ? null : await validateRuntime(root, value.pending);
|
|
105
|
+
return { ...value, active, pending };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function readControl(root) {
|
|
109
|
+
root = await canonicalManagerRoot(root);
|
|
110
|
+
if (!await managedDirectory(root)) return null;
|
|
111
|
+
const value = await readManagedJson(path.join(root, "control.json"));
|
|
112
|
+
return value === undefined ? null : validateControl(root, value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Caller holds withManagerLock for any read-modify-write transaction. */
|
|
116
|
+
export async function writeControl(root, value) {
|
|
117
|
+
root = await canonicalManagerRoot(root);
|
|
118
|
+
const validated = await validateControl(root, value);
|
|
119
|
+
await managedDirectory(root, { create: true });
|
|
120
|
+
await writeManagedJson(path.join(root, "control.json"), validated);
|
|
121
|
+
return validated;
|
|
122
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { activatePending } from "./activation.mjs";
|
|
2
|
+
import { downloadRelease, fetchRelease, newerVersion } from "./download.mjs";
|
|
3
|
+
import { withManagerLock } from "./mutex.mjs";
|
|
4
|
+
import { readControl, writeControl } from "./state.mjs";
|
|
5
|
+
|
|
6
|
+
import { checkDue, networkDisabled } from "./policy.mjs";
|
|
7
|
+
export { CHECK_INTERVAL_MS, networkDisabled } from "./policy.mjs";
|
|
8
|
+
|
|
9
|
+
/** One pass. The worker lock owns scheduling; admission is held only for state changes. */
|
|
10
|
+
export async function performUpdate(root, { force = false, check = false, env = process.env,
|
|
11
|
+
discover = fetchRelease, download = downloadRelease, activate = activatePending,
|
|
12
|
+
ignorePid = null } = {}) {
|
|
13
|
+
const initial = await readControl(root);
|
|
14
|
+
if (initial === null) throw new Error("ACC is not enrolled; run acc install first");
|
|
15
|
+
const data = { running: initial.active.version, auto: initial.auto, pin: initial.pin,
|
|
16
|
+
pending: initial.pending?.version ?? null, notice: initial.notice };
|
|
17
|
+
if (!force && !check && !initial.auto) return { ...data, checked: false, reason: "auto_off" };
|
|
18
|
+
// Recovery is local; the hard no-network override must not strand a partial refresh.
|
|
19
|
+
if (initial.pending && !check && (force || !networkDisabled(env))) {
|
|
20
|
+
return { ...data, ...await activate(root, { env, ignorePid }), checked: false };
|
|
21
|
+
}
|
|
22
|
+
if (networkDisabled(env)) return { ...data, checked: false, reason: "network_disabled" };
|
|
23
|
+
if (!force && !check && !checkDue(initial)) return { ...data, checked: false, reason: "not_due" };
|
|
24
|
+
if (!check) await withManagerLock(root, async () => {
|
|
25
|
+
const current = await readControl(root);
|
|
26
|
+
await writeControl(root, { ...current, checkedAt: new Date().toISOString() });
|
|
27
|
+
});
|
|
28
|
+
const release = await discover({ pin: initial.pin, env });
|
|
29
|
+
const newer = newerVersion(release.version, initial.active.version);
|
|
30
|
+
const checked = { ...data, checked: true, latest: release.version, newer };
|
|
31
|
+
if (check || !newer) return checked;
|
|
32
|
+
const generation = await download(root, release, { env });
|
|
33
|
+
const published = await withManagerLock(root, async () => {
|
|
34
|
+
const current = await readControl(root);
|
|
35
|
+
if (current.phase !== "ready" || current.active.root !== initial.active.root
|
|
36
|
+
|| current.pin !== initial.pin || !force && !current.auto) return false;
|
|
37
|
+
await writeControl(root, { ...current, pending: generation,
|
|
38
|
+
notice: `ACC ${generation.version} is downloaded and verified.` });
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
if (!published) return { ...checked, reason: "state_changed" };
|
|
42
|
+
return { ...checked, pending: generation.version, ...await activate(root, { env, ignorePid }) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A single detached worker may wait cheaply for native clients to exit. */
|
|
46
|
+
export async function runWorker(root, { force = false, env = process.env, wait = false, ignorePid = null } = {}) {
|
|
47
|
+
return withManagerLock(`${root}/worker`, async () => {
|
|
48
|
+
for (;;) {
|
|
49
|
+
const result = await performUpdate(root, { force, env, ignorePid });
|
|
50
|
+
if (!wait || result.reason !== "processes_active") return result;
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 60_000));
|
|
52
|
+
if (!(await readControl(root))?.auto || networkDisabled(env)) return result;
|
|
53
|
+
}
|
|
54
|
+
}, { timeoutMs: 100 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function recordWorkerFailure(root, error) {
|
|
58
|
+
// A second scheduled worker losing the lock is ordinary, not a failed update.
|
|
59
|
+
if (/manager lock held/.test(error?.message ?? "")) return;
|
|
60
|
+
await withManagerLock(root, async () => {
|
|
61
|
+
const control = await readControl(root);
|
|
62
|
+
if (control) await writeControl(root, { ...control,
|
|
63
|
+
notice: "Automatic update could not finish; the previous runtime was kept. Run acc update for details." });
|
|
64
|
+
}).catch(() => {});
|
|
65
|
+
}
|
|
@@ -1,126 +1,51 @@
|
|
|
1
|
-
import { listSessionBindings } from "@agents-can-communicate/adapter-sdk";
|
|
2
1
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Every mutating command needs a session id and the generation that proves the
|
|
8
|
-
* caller is that session. Until this existed, both had to be passed on the
|
|
9
|
-
* command line, and the shipped skills told agents to pass
|
|
10
|
-
* `--session "$ACC_SESSION" --generation "$ACC_GENERATION"` - two variables that
|
|
11
|
-
* nothing in the system has ever set. The generation is not printed by `acc
|
|
12
|
-
* status` either, deliberately: it is the proof of ownership, not public
|
|
13
|
-
* information. So the documented workflow could not be carried out by any agent
|
|
14
|
-
* on any client, and the only way through was to read ACC's own files by hand,
|
|
15
|
-
* which the same skill forbids in as many words.
|
|
16
|
-
*
|
|
17
|
-
* The hook runtime already knows both. It writes them into a binding at
|
|
18
|
-
* SessionStart, keyed by the harness's own session id. What was missing was a
|
|
19
|
-
* way for a shell inside that session to find its own binding. Each step below
|
|
20
|
-
* answers that from something the caller demonstrably has.
|
|
21
|
-
*/
|
|
3
|
+
// Inbox retrieves bodies and advances receipts, so it needs the same owner
|
|
4
|
+
// proof as a mutation. Public observations never need somebody else's token.
|
|
22
5
|
const NEEDS_OWNER = Object.freeze(new Set(["work", "claim", "release", "message",
|
|
23
|
-
"inbox", "reply", "request", "ack", "
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Reads that are answers about *you*.
|
|
27
|
-
*
|
|
28
|
-
* Attention is computed per session: what is addressed to you, what has become
|
|
29
|
-
* unblocked for you. Run by hand with nothing identifying the caller, both of
|
|
30
|
-
* these answered for nobody and returned an empty list - so an agent told at the
|
|
31
|
-
* top of its turn that work was waiting could ask `acc status` and be told there
|
|
32
|
-
* was none. They resolve softly: a shell with no session attached still gets the
|
|
33
|
-
* roster and the events, which is what it came for.
|
|
34
|
-
*/
|
|
6
|
+
"inbox", "reply", "request", "ack", "finish"]));
|
|
35
7
|
const WANTS_OWNER = Object.freeze(new Set(["sync", "status"]));
|
|
36
8
|
|
|
37
9
|
export const needsOwner = command => NEEDS_OWNER.has(command);
|
|
38
10
|
|
|
11
|
+
const unresolved = command => new AccError(EXIT.USAGE,
|
|
12
|
+
`${command} could not tell which session is running it. Pass both --session and `
|
|
13
|
+
+ "--generation from this turn's ACC hook header or your own acc attach, or use this "
|
|
14
|
+
+ "session's ACC MCP tools. Do not copy a peer's session from acc status. Native "
|
|
15
|
+
+ "session IDs and a shared checkout do not establish CLI ownership.",
|
|
16
|
+
{ command, reasonCode: "caller_identity_unresolved" });
|
|
17
|
+
|
|
39
18
|
/**
|
|
40
|
-
*
|
|
19
|
+
* Accept caller-supplied credentials; never discover credentials from peers.
|
|
41
20
|
*
|
|
42
|
-
* A binding
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
21
|
+
* A sole binding, a matching checkout, and a public session selector all let an
|
|
22
|
+
* unbound client adopt another session. Native environment values do too:
|
|
23
|
+
* nested clients inherit their parent's IDs. Bindings identify hook payloads,
|
|
24
|
+
* not arbitrary shell callers. ACC_SESSION/ACC_GENERATION remain an explicit
|
|
25
|
+
* operator-supplied pair, not an assertion that environment provenance is known.
|
|
46
26
|
*/
|
|
47
|
-
async function liveCandidates({ context }) {
|
|
48
|
-
const bindings = await listSessionBindings({ runtimeDir: context.paths.root });
|
|
49
|
-
if (bindings.length === 0) return [];
|
|
50
|
-
const status = await context.service.collectStatus({});
|
|
51
|
-
const live = new Map(status.participants
|
|
52
|
-
.filter(participant => participant.presence !== "offline")
|
|
53
|
-
.map(participant => [participant.sessionId, participant]));
|
|
54
|
-
return bindings
|
|
55
|
-
.filter(binding => live.has(binding.accSessionId))
|
|
56
|
-
.map(binding => ({ ...binding, session: live.get(binding.accSessionId) }));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const describe = candidates => candidates
|
|
60
|
-
.map(candidate => `${candidate.session.participantId} (${candidate.session.harness})`)
|
|
61
|
-
.join(", ");
|
|
62
|
-
|
|
63
27
|
export async function resolveOwner({ command, options, context, env = {} }) {
|
|
64
28
|
const soft = WANTS_OWNER.has(command);
|
|
65
29
|
if (!soft && !needsOwner(command)) return options;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const candidates = await liveCandidates({ context });
|
|
70
|
-
const take = candidate => ({ ...options, session: candidate.accSessionId,
|
|
71
|
-
generation: candidate.generation,
|
|
72
|
-
// `status` filters by participant rather than by session, so it is told who
|
|
73
|
-
// the caller is in the terms it works in.
|
|
74
|
-
participant: options.participant ?? candidate.session.participantId });
|
|
75
|
-
|
|
76
|
-
// Named a session but not the generation: the id is in `acc status --json`,
|
|
77
|
-
// so this is what an agent reaches for first, and refusing it would teach the
|
|
78
|
-
// wrong lesson about which half is secret.
|
|
79
|
-
if (options.session !== undefined) {
|
|
80
|
-
// The reads never needed a generation, and a session opened by `acc attach`
|
|
81
|
-
// has no binding to find one in. Answering for the session it was given is
|
|
82
|
-
// both what it was asked and what it did before this resolver existed.
|
|
30
|
+
const explicit = options.session !== undefined && options.generation !== undefined;
|
|
31
|
+
const configured = typeof env.ACC_SESSION === "string" && typeof env.ACC_GENERATION === "string";
|
|
32
|
+
if (!explicit && !configured) {
|
|
83
33
|
if (soft) return options;
|
|
84
|
-
|
|
85
|
-
if (named !== undefined) return take(named);
|
|
86
|
-
throw new AccError(EXIT.USAGE,
|
|
87
|
-
`no live session ${options.session} in this workspace`,
|
|
88
|
-
{ command, session: options.session });
|
|
34
|
+
throw unresolved(command);
|
|
89
35
|
}
|
|
90
36
|
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
37
|
+
const owner = explicit ? options : { session: env.ACC_SESSION, generation: env.ACC_GENERATION };
|
|
38
|
+
if (options.session !== undefined && options.session !== owner.session) {
|
|
39
|
+
// A public observation scope is allowed; it does not request credentials.
|
|
40
|
+
if (soft) return options;
|
|
41
|
+
throw unresolved(command);
|
|
94
42
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (byEnvironment.length === 1) return take(byEnvironment[0]);
|
|
104
|
-
|
|
105
|
-
// Two agents in one workspace are the normal case, and each is usually in its
|
|
106
|
-
// own checkout - that is what the worktree story is. Same repository, so the
|
|
107
|
-
// same workspace, and the checkout tells them apart.
|
|
108
|
-
const here = context.descriptor.git?.worktreeRoot;
|
|
109
|
-
const byCheckout = here === undefined
|
|
110
|
-
? []
|
|
111
|
-
: candidates.filter(candidate => candidate.session.checkoutRoot === here);
|
|
112
|
-
if (byCheckout.length === 1) return take(byCheckout[0]);
|
|
113
|
-
|
|
114
|
-
if (candidates.length === 1) return take(candidates[0]);
|
|
115
|
-
|
|
116
|
-
// Guessing between two live sessions would let one agent act as another, which
|
|
117
|
-
// is exactly what the generation exists to prevent. So this refuses, and says
|
|
118
|
-
// what it found.
|
|
119
|
-
if (soft) return options;
|
|
120
|
-
throw new AccError(EXIT.USAGE, candidates.length === 0
|
|
121
|
-
? `${command} could not tell which session is running it: no live session is `
|
|
122
|
-
+ "attached here. An adapter attaches one at SessionStart; `acc status` shows who is."
|
|
123
|
-
: `${command} could not tell which of ${candidates.length} live sessions is running `
|
|
124
|
-
+ `it (${describe(candidates)}). Pass --session, which \`acc status --json\` lists.`,
|
|
125
|
-
{ command, candidates: candidates.map(candidate => candidate.accSessionId) });
|
|
43
|
+
// A supplied generation must reach the core unchanged so stale calls fail.
|
|
44
|
+
const resolved = { ...options, session: owner.session,
|
|
45
|
+
generation: options.generation ?? owner.generation };
|
|
46
|
+
if (command === "status" && options.participant === undefined) {
|
|
47
|
+
const { participants } = await context.service.collectStatus({});
|
|
48
|
+
resolved.participant = participants.find(item => item.sessionId === owner.session)?.participantId;
|
|
49
|
+
}
|
|
50
|
+
return resolved;
|
|
126
51
|
}
|
|
@@ -7,6 +7,8 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
7
7
|
import { platformPaths } from "./platform-paths.mjs";
|
|
8
8
|
import { checkingIsOff, fetchLatest, isNewer, writeCachedCheck } from "./update-check.mjs";
|
|
9
9
|
|
|
10
|
+
import { runManagedUpdate, validateUpdateOptions } from "./managed-runtime/command.mjs";
|
|
11
|
+
|
|
10
12
|
const execFileAsync = promisify(execFile);
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -26,6 +28,14 @@ export const upgradeSteps = version => [
|
|
|
26
28
|
const spell = ([command, argv]) => ` ${command} ${argv.join(" ")}`;
|
|
27
29
|
|
|
28
30
|
export async function runUpdateCommand({ options, runtime }) {
|
|
31
|
+
if (options.check === true && options.apply === true) {
|
|
32
|
+
throw new AccError(EXIT.USAGE, "use either --check or --apply");
|
|
33
|
+
}
|
|
34
|
+
validateUpdateOptions(options);
|
|
35
|
+
if (runtime.managerRoot) return runManagedUpdate({ options, runtime });
|
|
36
|
+
if (options.auto !== undefined || options.pin !== undefined) {
|
|
37
|
+
throw new AccError(EXIT.USAGE, "run acc install before configuring automatic updates");
|
|
38
|
+
}
|
|
29
39
|
const env = runtime.env ?? {};
|
|
30
40
|
const { data: dataHome } = platformPaths({ platform: runtime.platform, env });
|
|
31
41
|
const running = typeof runtime.version === "function"
|
|
@@ -61,26 +71,34 @@ export async function runUpdateCommand({ options, runtime }) {
|
|
|
61
71
|
const data = { checked: true, running, latest, newer: true,
|
|
62
72
|
steps: steps.map(([command, argv]) => [command, ...argv].join(" ")) };
|
|
63
73
|
|
|
64
|
-
if (options.
|
|
74
|
+
if (options.check === true) {
|
|
65
75
|
return { data, text: [`acc ${latest} is available; you have ${running}`, "",
|
|
66
|
-
...steps.map(spell), "", "or run: acc update
|
|
76
|
+
...steps.map(spell), "", "or run: acc update"].join("\n") };
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
const spawn = runtime.spawn ?? ((command, argv) => execFileAsync(command, argv, { env }));
|
|
70
80
|
const done = [];
|
|
81
|
+
let installation = { stdout: "", stderr: "" };
|
|
71
82
|
for (const [command, argv] of steps) {
|
|
72
83
|
try {
|
|
73
|
-
await spawn(command, argv);
|
|
84
|
+
const output = await spawn(command, argv);
|
|
85
|
+
if (command === "acc") {
|
|
86
|
+
installation = { stdout: String(output?.stdout ?? "").trim(),
|
|
87
|
+
stderr: String(output?.stderr ?? "").trim() };
|
|
88
|
+
}
|
|
74
89
|
done.push([command, ...argv].join(" "));
|
|
75
90
|
} catch (error) {
|
|
76
91
|
// Named rather than swallowed, and the rest of the commands are printed:
|
|
77
92
|
// a global install refused for want of permission is the ordinary case,
|
|
78
93
|
// and the person can finish it by hand from here.
|
|
94
|
+
const text = [`${command} failed: ${error.message}`, "", "finish it with:",
|
|
95
|
+
...steps.slice(done.length).map(spell)].join("\n");
|
|
79
96
|
return { data: { ...data, applied: done, failed: [command, ...argv].join(" ") },
|
|
80
|
-
text:
|
|
81
|
-
...steps.slice(done.length).map(spell)].join("\n"),
|
|
82
|
-
error: undefined };
|
|
97
|
+
text, error: new AccError(EXIT.DATA, text) };
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
|
-
|
|
100
|
+
const activation = "Restart all running agent clients to load the updated ACC runtime and skills.";
|
|
101
|
+
return { data: { ...data, applied: done, installation, activation },
|
|
102
|
+
text: [`updated to ${latest}`, installation.stdout, installation.stderr, activation]
|
|
103
|
+
.filter(Boolean).join("\n\n") };
|
|
86
104
|
}
|
|
@@ -2,6 +2,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
2
2
|
|
|
3
3
|
import { overlaps } from "./claims.mjs";
|
|
4
4
|
import { classifySessionPresence } from "./sessions.mjs";
|
|
5
|
+
import { decisionView, isCurrentDecision } from "./decision-state.mjs";
|
|
5
6
|
|
|
6
7
|
export const ATTENTION_PRIORITY = Object.freeze({
|
|
7
8
|
reply_required: 1,
|
|
@@ -95,6 +96,8 @@ export function computeAttention(snapshot, { session, participantId, now, pidIsA
|
|
|
95
96
|
if (typeof pidIsAlive !== "function") {
|
|
96
97
|
throw new AccError(EXIT.USAGE, "computeAttention requires a pidIsAlive probe", {});
|
|
97
98
|
}
|
|
99
|
+
snapshot = { ...snapshot, messages: (snapshot.messages ?? [])
|
|
100
|
+
.map(decisionView(snapshot.messages ?? [])).filter(isCurrentDecision) };
|
|
98
101
|
return [
|
|
99
102
|
...obligationItems(snapshot, participantId),
|
|
100
103
|
...unavailableRecipients(snapshot, participantId, now, pidIsAlive),
|
|
@@ -33,17 +33,19 @@ const isLive = (claim, now) => Date.parse(claim.expiresAt) > Date.parse(now);
|
|
|
33
33
|
export function createClaimService(ports, sessions) {
|
|
34
34
|
const { store, clock, ids, pidIsAlive } = ports;
|
|
35
35
|
|
|
36
|
-
async function requireOwner(input, action) {
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
async function requireOwner(input, action, tx) {
|
|
37
|
+
const current = tx === undefined
|
|
38
|
+
? (await sessions.locateSession(input.sessionId, input.workspaceId))?.record
|
|
39
|
+
: tx.get("session", input.sessionId);
|
|
40
|
+
if (current == null || current.state !== "open") {
|
|
39
41
|
throw new AccError(EXIT.CONFLICT, `cannot ${action} from a session that is not open`,
|
|
40
42
|
{ sessionId: input.sessionId });
|
|
41
43
|
}
|
|
42
|
-
if (
|
|
44
|
+
if (current.generation !== input.generation) {
|
|
43
45
|
throw new AccError(EXIT.CONFLICT, `cannot ${action} with a replaced session generation`,
|
|
44
46
|
{ sessionId: input.sessionId });
|
|
45
47
|
}
|
|
46
|
-
return
|
|
48
|
+
return current;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function conflictWith(existing, owner, now, snapshotSessions) {
|
|
@@ -83,6 +85,7 @@ export function createClaimService(ports, sessions) {
|
|
|
83
85
|
|
|
84
86
|
let record = null;
|
|
85
87
|
await store.transaction(async tx => {
|
|
88
|
+
await requireOwner(input, "claim", tx);
|
|
86
89
|
const live = tx.list("claim", claim => isLive(claim, now));
|
|
87
90
|
// This session's own claim on this resource, live or lapsed. Looking only
|
|
88
91
|
// among the live ones left the expired record behind and made a fresh one
|
|
@@ -117,7 +120,7 @@ export function createClaimService(ports, sessions) {
|
|
|
117
120
|
actorSessionId: session.sessionId, type: mine === undefined ? "claim.acquired"
|
|
118
121
|
: "claim.renewed", occurredAt: now,
|
|
119
122
|
payload: { claimId, resource, mode: record.mode } });
|
|
120
|
-
}, { kinds: ["claim"] });
|
|
123
|
+
}, { kinds: ["session", "claim"] });
|
|
121
124
|
return record;
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -128,6 +131,7 @@ export function createClaimService(ports, sessions) {
|
|
|
128
131
|
.toISOString();
|
|
129
132
|
let record = null;
|
|
130
133
|
await store.transaction(async tx => {
|
|
134
|
+
await requireOwner(input, "renew", tx);
|
|
131
135
|
const existing = tx.get("claim", input.claimId);
|
|
132
136
|
if (existing === null || existing.ownerSessionId !== session.sessionId) {
|
|
133
137
|
throw new AccError(EXIT.CONFLICT, "only the owning session generation may renew a claim",
|
|
@@ -138,7 +142,7 @@ export function createClaimService(ports, sessions) {
|
|
|
138
142
|
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
139
143
|
workspaceId: existing.workspaceId, actorSessionId: session.sessionId,
|
|
140
144
|
type: "claim.renewed", occurredAt: now, payload: { claimId: input.claimId } });
|
|
141
|
-
}, { kinds: ["claim"] });
|
|
145
|
+
}, { kinds: ["session", "claim"] });
|
|
142
146
|
return record;
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -146,6 +150,7 @@ export function createClaimService(ports, sessions) {
|
|
|
146
150
|
const session = await requireOwner(input, "release");
|
|
147
151
|
const now = clock.now();
|
|
148
152
|
await store.transaction(async tx => {
|
|
153
|
+
await requireOwner(input, "release", tx);
|
|
149
154
|
const existing = tx.get("claim", input.claimId);
|
|
150
155
|
if (existing === null) {
|
|
151
156
|
throw new AccError(EXIT.CONFLICT, "the claim does not exist", { claimId: input.claimId });
|
|
@@ -167,7 +172,7 @@ export function createClaimService(ports, sessions) {
|
|
|
167
172
|
type: owned ? "claim.released" : "claim.force_released", occurredAt: now,
|
|
168
173
|
payload: { claimId: input.claimId, authority: authority ?? null,
|
|
169
174
|
reason: reason ?? null, replacedGeneration: existing.generation } });
|
|
170
|
-
}, { kinds: ["claim"] });
|
|
175
|
+
}, { kinds: ["session", "claim"] });
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
return {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
|
|
4
3
|
import { AccError, EXIT, SCHEMA_VERSION, validateRecord }
|
|
5
4
|
from "@agents-can-communicate/protocol";
|
|
6
5
|
|
|
7
6
|
import { projectReleasedClaim, reconstructFinishRetry } from "./finish-retries.mjs";
|
|
8
7
|
import { ensureMaterialised } from "./materialisation.mjs";
|
|
8
|
+
import { nextTurnDelivery as selectNextTurnDelivery } from "./next-turn-delivery.mjs";
|
|
9
|
+
import { prepareDecisionChange } from "./decision-changes.mjs";
|
|
9
10
|
|
|
10
11
|
export const receiptId = (messageId, participantId) => `receipt_${createHash("sha256")
|
|
11
12
|
.update(JSON.stringify([messageId, participantId])).digest("base64url")}`;
|
|
@@ -19,6 +20,7 @@ const logicalContent = message => ({
|
|
|
19
20
|
inReplyTo: message.inReplyTo,
|
|
20
21
|
artifacts: message.artifacts,
|
|
21
22
|
handoff: message.handoff,
|
|
23
|
+
decisionChange: message.decisionChange ?? null,
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
const normalizedContent = input => logicalContent({
|
|
@@ -30,6 +32,7 @@ const normalizedContent = input => logicalContent({
|
|
|
30
32
|
inReplyTo: input.inReplyTo ?? null,
|
|
31
33
|
artifacts: input.artifacts ?? [],
|
|
32
34
|
handoff: input.handoff ?? null,
|
|
35
|
+
decisionChange: input.decisionChange,
|
|
33
36
|
});
|
|
34
37
|
|
|
35
38
|
function existingMessage(tx, session, input) {
|
|
@@ -55,6 +58,41 @@ function assertCurrentSession(tx, session, action) {
|
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Turn what the sender typed into participant ids that exist.
|
|
63
|
+
*
|
|
64
|
+
* An exact participant id is always taken as written. A name that matches no
|
|
65
|
+
* participant is tried as a client name - `codex`, `claude_code` - because that
|
|
66
|
+
* is what an agent knows about its peers: the roster it is handed names the
|
|
67
|
+
* client, and asking it to first look up `codex-fbqX8o` is a lookup it
|
|
68
|
+
* routinely skips. Every shipped example taught this form while nothing
|
|
69
|
+
* implemented it, so an agent following its own skill was refused on its first
|
|
70
|
+
* attempt to reach anyone.
|
|
71
|
+
*
|
|
72
|
+
* One live session of that client resolves. Two refuse and name them, the way
|
|
73
|
+
* `--session` already refuses to choose between two sessions of one client:
|
|
74
|
+
* guessing is how a question is answered by a session that never had the
|
|
75
|
+
* context. The sender's own client is excluded, or the one client an agent is
|
|
76
|
+
* certain is running becomes the easiest thing to address by accident.
|
|
77
|
+
*/
|
|
78
|
+
function resolveAddressees(tx, requested, session) {
|
|
79
|
+
const known = new Set(tx.list("participant").map(item => item.participantId));
|
|
80
|
+
return requested.map(name => {
|
|
81
|
+
if (known.has(name)) return name;
|
|
82
|
+
const peers = [...new Set(tx.list("session", item => item.state === "open"
|
|
83
|
+
&& item.harness === name && item.participantId !== session.participantId)
|
|
84
|
+
.map(item => item.participantId))].sort();
|
|
85
|
+
if (peers.length === 1) return peers[0];
|
|
86
|
+
if (peers.length > 1) {
|
|
87
|
+
throw new AccError(EXIT.USAGE,
|
|
88
|
+
`could not tell which of ${peers.length} ${name} sessions you meant: `
|
|
89
|
+
+ `${peers.join(", ")}. Name one of them, which \`acc status --json\` lists.`,
|
|
90
|
+
{ client: name, candidates: peers });
|
|
91
|
+
}
|
|
92
|
+
return name;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
58
96
|
function addressedRecipients(tx, message, session) {
|
|
59
97
|
if (message.toParticipantIds.length === 0) {
|
|
60
98
|
return [...new Set(tx.list("session", item => item.state === "open"
|
|
@@ -86,9 +124,14 @@ function threadFor(tx, messageId, inReplyTo) {
|
|
|
86
124
|
return parent.threadId;
|
|
87
125
|
}
|
|
88
126
|
|
|
89
|
-
export function recordMessageInTransaction({ tx, session, input, now, messageId, ids,
|
|
127
|
+
export function recordMessageInTransaction({ tx, session, input: raw, now, messageId, ids,
|
|
90
128
|
action = "send a message" }) {
|
|
91
129
|
assertCurrentSession(tx, session, action);
|
|
130
|
+
// Resolved before anything else reads the recipients, so the stored message
|
|
131
|
+
// names real participants and a retry compares against the same content. A
|
|
132
|
+
// record that kept the client name would address nobody when it is read back.
|
|
133
|
+
const input = prepareDecisionChange(tx, { ...raw,
|
|
134
|
+
toParticipantIds: resolveAddressees(tx, raw.toParticipantIds ?? [], session) }, session);
|
|
92
135
|
const existing = existingMessage(tx, session, input);
|
|
93
136
|
if (existing !== undefined) {
|
|
94
137
|
return { message: existing, recipientParticipantIds: [], created: false };
|
|
@@ -111,6 +154,7 @@ export function recordMessageInTransaction({ tx, session, input, now, messageId,
|
|
|
111
154
|
inReplyTo,
|
|
112
155
|
artifacts: input.artifacts ?? [],
|
|
113
156
|
handoff: input.handoff ?? null,
|
|
157
|
+
...(input.decisionChange === undefined ? {} : { decisionChange: input.decisionChange }),
|
|
114
158
|
sentAt: now,
|
|
115
159
|
});
|
|
116
160
|
const recipientParticipantIds = addressedRecipients(tx, message, session);
|
|
@@ -248,28 +292,8 @@ export function createConversationService(ports, sessions) {
|
|
|
248
292
|
return (await nextTurnDelivery(input)).queuedMessages;
|
|
249
293
|
}
|
|
250
294
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return { queuedMessages: [], liveOfferedMessageIds: [], roomMessageIds: [] };
|
|
254
|
-
}
|
|
255
|
-
const snapshot = await store.snapshot(input.workspaceId ?? store.workspaceId,
|
|
256
|
-
{ kinds: ["message", "receipt"] });
|
|
257
|
-
const receiptByMessage = new Map(snapshot.receipts
|
|
258
|
-
.filter(item => item.recipientParticipantId === input.participantId)
|
|
259
|
-
.map(item => [item.messageId, item]));
|
|
260
|
-
const eligible = snapshot.messages.filter(message => receiptByMessage.has(message.messageId)
|
|
261
|
-
&& message.fromSessionId !== input.exceptSessionId)
|
|
262
|
-
.sort((left, right) => left.sentAt.localeCompare(right.sentAt)
|
|
263
|
-
|| left.messageId.localeCompare(right.messageId));
|
|
264
|
-
return {
|
|
265
|
-
queuedMessages: eligible.filter(message =>
|
|
266
|
-
receiptByMessage.get(message.messageId).state === "queued"),
|
|
267
|
-
liveOfferedMessageIds: eligible.filter(message => message.toParticipantIds.length > 0
|
|
268
|
-
&& receiptByMessage.get(message.messageId).state === "offered")
|
|
269
|
-
.map(message => message.messageId),
|
|
270
|
-
roomMessageIds: eligible.filter(message => message.toParticipantIds.length === 0)
|
|
271
|
-
.map(message => message.messageId),
|
|
272
|
-
};
|
|
295
|
+
function nextTurnDelivery(input) {
|
|
296
|
+
return selectNextTurnDelivery(store, input);
|
|
273
297
|
}
|
|
274
298
|
|
|
275
299
|
return { sendMessage, finishSession, pendingMessages, nextTurnDelivery };
|