agents-can-communicate 0.3.1 → 0.4.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 +95 -52
- package/SECURITY.md +1 -1
- 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 +77 -0
- package/docs/ADAPTER_AUTHORING.md +52 -8
- package/docs/ARCHITECTURE.md +64 -11
- package/docs/CAPABILITIES.md +41 -37
- package/docs/CLI.md +145 -25
- package/docs/CONCEPTS.md +13 -6
- package/docs/CONFIGURATION.md +37 -22
- package/docs/DESIGN_DECISIONS.md +16 -11
- package/docs/GETTING_STARTED.md +41 -7
- package/docs/GLOSSARY.md +9 -3
- package/docs/HOW_IT_WORKS.md +32 -11
- package/docs/MCP.md +91 -24
- package/docs/PROTOCOL.md +69 -12
- package/docs/RELEASING.md +28 -10
- package/docs/SECURITY_MODEL.md +47 -13
- package/docs/TROUBLESHOOTING.md +102 -20
- package/docs/UPGRADING.md +154 -0
- package/docs/WHY_ACC.md +5 -4
- package/docs/index.md +8 -0
- 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 +106 -5
- 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 +106 -5
- 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 +106 -5
- 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 +122 -8
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +5 -3
- package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +9 -0
- 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 +106 -5
- 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 +24 -5
- 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 +64 -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 +78 -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 +123 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +1 -1
- 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 +9 -25
- 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/owner-context.mjs +30 -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 +172 -67
- package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- 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
|
@@ -40,7 +40,7 @@ export function runtimePaths({ dataHome, workspaceId, workspaceRoots = [] }) {
|
|
|
40
40
|
`${workspaceRoot} holds ACC's own state at ${path.join(dataHome, "acc")}, `
|
|
41
41
|
+ "so it cannot be a workspace. Run acc inside a project, or point "
|
|
42
42
|
+ "ACC_DATA_HOME outside this directory.",
|
|
43
|
-
{ root, dataHome, workspaceRoot });
|
|
43
|
+
{ root, dataHome, workspaceRoot, reasonCode: "workspace_contains_runtime" });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
return Object.freeze(Object.fromEntries([["root", root],
|
|
@@ -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) {
|
|
@@ -127,8 +130,8 @@ export function recordMessageInTransaction({ tx, session, input: raw, now, messa
|
|
|
127
130
|
// Resolved before anything else reads the recipients, so the stored message
|
|
128
131
|
// names real participants and a retry compares against the same content. A
|
|
129
132
|
// record that kept the client name would address nobody when it is read back.
|
|
130
|
-
const input = { ...raw,
|
|
131
|
-
toParticipantIds: resolveAddressees(tx, raw.toParticipantIds ?? [], session) };
|
|
133
|
+
const input = prepareDecisionChange(tx, { ...raw,
|
|
134
|
+
toParticipantIds: resolveAddressees(tx, raw.toParticipantIds ?? [], session) }, session);
|
|
132
135
|
const existing = existingMessage(tx, session, input);
|
|
133
136
|
if (existing !== undefined) {
|
|
134
137
|
return { message: existing, recipientParticipantIds: [], created: false };
|
|
@@ -151,6 +154,7 @@ export function recordMessageInTransaction({ tx, session, input: raw, now, messa
|
|
|
151
154
|
inReplyTo,
|
|
152
155
|
artifacts: input.artifacts ?? [],
|
|
153
156
|
handoff: input.handoff ?? null,
|
|
157
|
+
...(input.decisionChange === undefined ? {} : { decisionChange: input.decisionChange }),
|
|
154
158
|
sentAt: now,
|
|
155
159
|
});
|
|
156
160
|
const recipientParticipantIds = addressedRecipients(tx, message, session);
|
|
@@ -288,28 +292,8 @@ export function createConversationService(ports, sessions) {
|
|
|
288
292
|
return (await nextTurnDelivery(input)).queuedMessages;
|
|
289
293
|
}
|
|
290
294
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return { queuedMessages: [], liveOfferedMessageIds: [], roomMessageIds: [] };
|
|
294
|
-
}
|
|
295
|
-
const snapshot = await store.snapshot(input.workspaceId ?? store.workspaceId,
|
|
296
|
-
{ kinds: ["message", "receipt"] });
|
|
297
|
-
const receiptByMessage = new Map(snapshot.receipts
|
|
298
|
-
.filter(item => item.recipientParticipantId === input.participantId)
|
|
299
|
-
.map(item => [item.messageId, item]));
|
|
300
|
-
const eligible = snapshot.messages.filter(message => receiptByMessage.has(message.messageId)
|
|
301
|
-
&& message.fromSessionId !== input.exceptSessionId)
|
|
302
|
-
.sort((left, right) => left.sentAt.localeCompare(right.sentAt)
|
|
303
|
-
|| left.messageId.localeCompare(right.messageId));
|
|
304
|
-
return {
|
|
305
|
-
queuedMessages: eligible.filter(message =>
|
|
306
|
-
receiptByMessage.get(message.messageId).state === "queued"),
|
|
307
|
-
liveOfferedMessageIds: eligible.filter(message => message.toParticipantIds.length > 0
|
|
308
|
-
&& receiptByMessage.get(message.messageId).state === "offered")
|
|
309
|
-
.map(message => message.messageId),
|
|
310
|
-
roomMessageIds: eligible.filter(message => message.toParticipantIds.length === 0)
|
|
311
|
-
.map(message => message.messageId),
|
|
312
|
-
};
|
|
295
|
+
function nextTurnDelivery(input) {
|
|
296
|
+
return selectNextTurnDelivery(store, input);
|
|
313
297
|
}
|
|
314
298
|
|
|
315
299
|
return { sendMessage, finishSession, pendingMessages, nextTurnDelivery };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AccError, EXIT, assertDecisionChange } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
// Resolve inherited recipients before retry comparison. Target messages and
|
|
4
|
+
// their recipient sets are immutable, so receipt advancement cannot alter a retry.
|
|
5
|
+
export function prepareDecisionChange(tx, input, session) {
|
|
6
|
+
if (input.decisionChange !== undefined) {
|
|
7
|
+
throw new AccError(EXIT.USAGE, "send a decision change using supersedes or withdraws");
|
|
8
|
+
}
|
|
9
|
+
if (input.supersedes === undefined && input.withdraws === undefined) return input;
|
|
10
|
+
if (input.kind !== "decision" || (input.supersedes !== undefined && input.withdraws !== undefined)) {
|
|
11
|
+
throw new AccError(EXIT.USAGE, "only decisions accept either supersedes or withdraws, never both");
|
|
12
|
+
}
|
|
13
|
+
const decisionChange = assertDecisionChange({
|
|
14
|
+
action: input.supersedes === undefined ? "withdraw" : "replace",
|
|
15
|
+
messageIds: input.supersedes ?? input.withdraws,
|
|
16
|
+
});
|
|
17
|
+
decisionChange.messageIds = [...decisionChange.messageIds].sort();
|
|
18
|
+
const targets = decisionChange.messageIds.map(messageId => {
|
|
19
|
+
const message = tx.get("message", messageId);
|
|
20
|
+
if (message?.kind !== "decision" || message.workspaceId !== session.workspaceId) {
|
|
21
|
+
throw new AccError(EXIT.DATA, "a decision change must reference existing decisions in this workspace",
|
|
22
|
+
{ messageId });
|
|
23
|
+
}
|
|
24
|
+
return message;
|
|
25
|
+
});
|
|
26
|
+
if (new Set(input.toParticipantIds).size !== input.toParticipantIds.length) {
|
|
27
|
+
throw new AccError(EXIT.USAGE, "a participant may be addressed only once");
|
|
28
|
+
}
|
|
29
|
+
const targetIds = new Set(decisionChange.messageIds);
|
|
30
|
+
const inherited = [...targets.map(message => message.fromParticipantId),
|
|
31
|
+
...tx.list("receipt", receipt => targetIds.has(receipt.messageId))
|
|
32
|
+
.map(receipt => receipt.recipientParticipantId)]
|
|
33
|
+
.filter(participantId => participantId !== session.participantId);
|
|
34
|
+
return { ...input, decisionChange,
|
|
35
|
+
toParticipantIds: [...new Set([...input.toParticipantIds, ...inherited])].sort() };
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Lifecycle is a read model of explicit peer assertions, never a timestamp
|
|
2
|
+
// winner or a consensus verdict. Iterative traversal avoids recursion limits.
|
|
3
|
+
export function decisionView(messages) {
|
|
4
|
+
const decisions = new Map(messages.filter(m => m.kind === "decision").map(m => [m.messageId, m]));
|
|
5
|
+
const neighbours = new Map([...decisions.keys()].map(id => [id, new Set()]));
|
|
6
|
+
const replaced = new Set();
|
|
7
|
+
for (const message of decisions.values()) {
|
|
8
|
+
for (const target of message.decisionChange?.messageIds ?? []) {
|
|
9
|
+
if (!decisions.has(target)) continue;
|
|
10
|
+
neighbours.get(target).add(message.messageId);
|
|
11
|
+
neighbours.get(message.messageId).add(target);
|
|
12
|
+
replaced.add(target);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const statuses = new Map();
|
|
16
|
+
for (const start of decisions.keys()) {
|
|
17
|
+
if (statuses.has(start)) continue;
|
|
18
|
+
const group = new Set([start]), pending = [start];
|
|
19
|
+
while (pending.length > 0) {
|
|
20
|
+
for (const id of neighbours.get(pending.pop())) {
|
|
21
|
+
if (!group.has(id)) { group.add(id); pending.push(id); }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const heads = [...group].filter(id => !replaced.has(id));
|
|
25
|
+
const hasDecision = heads.some(id => decisions.get(id).decisionChange?.action !== "withdraw");
|
|
26
|
+
const roots = [...group].filter(id => decisions.get(id).decisionChange === undefined);
|
|
27
|
+
const groupId = (roots.length > 0 ? roots : [...group]).sort()[0];
|
|
28
|
+
for (const id of group) {
|
|
29
|
+
const isHead = !replaced.has(id);
|
|
30
|
+
statuses.set(id, {
|
|
31
|
+
state: isHead ? (decisions.get(id).decisionChange?.action === "withdraw" ? "withdrawn" : "current")
|
|
32
|
+
: (hasDecision ? "superseded" : "withdrawn"),
|
|
33
|
+
isHead, conflicted: heads.length !== 1, headCount: heads.length,
|
|
34
|
+
currentMessageId: heads.length === 1 ? heads[0] : null, groupId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return message => statuses.has(message.messageId)
|
|
39
|
+
? { ...message, decisionStatus: statuses.get(message.messageId) } : message;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const isCurrentDecision = message => message.decisionStatus?.isHead !== false;
|
|
@@ -81,18 +81,24 @@ export function createDeliveryBindingService(ports, sessions) {
|
|
|
81
81
|
// delete() takes the same mutex, so this is the one atomic form: a stale
|
|
82
82
|
// generation cannot retire a successor's endpoint, and an absent or
|
|
83
83
|
// already-retired binding stays as it is.
|
|
84
|
+
// A caller that captured an endpoint may also scope retirement to that exact
|
|
85
|
+
// address. This protects a same-generation re-handshake that publishes while
|
|
86
|
+
// an older bounded cleanup call is still waiting to enter the writer lock.
|
|
84
87
|
//
|
|
85
88
|
// The retirement is recorded as its own fact rather than as an expired lease.
|
|
86
89
|
// Expiry and retirement used to be the same edit, so a channel still renewing
|
|
87
90
|
// its endpoint could extend a binding the session had already given up.
|
|
88
|
-
async function clearDeliveryBinding(
|
|
91
|
+
async function clearDeliveryBinding(input) {
|
|
92
|
+
const { sessionId, generation } = input;
|
|
93
|
+
const endpointScoped = Object.hasOwn(input, "opaqueEndpointRef");
|
|
89
94
|
await store.ephemeral.update("deliveryBinding", sessionId, async current => {
|
|
90
95
|
if (current === null || current === undefined) return null;
|
|
91
96
|
if (current.generation !== generation) throw conflict(sessionId);
|
|
97
|
+
if (endpointScoped && current.opaqueEndpointRef !== input.opaqueEndpointRef) return null;
|
|
92
98
|
if (current.retiredAt !== null && current.retiredAt !== undefined) return null;
|
|
93
99
|
const now = clock.now();
|
|
94
100
|
return { ...current, leaseUntil: now, retiredAt: now };
|
|
95
|
-
});
|
|
101
|
+
}, { deadlineAt: input.deadlineAt });
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
/**
|
|
@@ -120,9 +126,10 @@ export function createDeliveryBindingService(ports, sessions) {
|
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
async function listDeliveryBindings({ participantId, now }) {
|
|
129
|
+
async function listDeliveryBindings({ participantId, now, includeExpired = false }) {
|
|
124
130
|
return (await currentBindings(now))
|
|
125
|
-
.filter(item => item.participantId === participantId
|
|
131
|
+
.filter(item => item.participantId === participantId
|
|
132
|
+
&& (includeExpired || item.reachable))
|
|
126
133
|
.map(item => item.binding);
|
|
127
134
|
}
|
|
128
135
|
|