agents-can-communicate 0.1.18 → 0.3.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 +87 -69
- package/SECURITY.md +31 -0
- package/bin/acc-bootstrap.mjs +56 -0
- package/bin/acc-claude-channel.mjs +177 -0
- package/bin/acc-hook.mjs +94 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +13 -3
- package/docs/ADAPTER_AUTHORING.md +204 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +117 -214
- package/docs/CLI.md +164 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +147 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/HOW_IT_WORKS.md +277 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +115 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +108 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +269 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +150 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +199 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +11 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +68 -0
- 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 +22 -22
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +31 -13
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +28 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +131 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +131 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +190 -105
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
- package/package.json +20 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -14,7 +14,7 @@ import { COMMANDS } from "./args.mjs";
|
|
|
14
14
|
const GROUPS = Object.freeze([
|
|
15
15
|
["Set up", ["install", "uninstall", "doctor", "config"]],
|
|
16
16
|
["In a session", ["status", "sync", "work", "claim", "release", "inbox", "reply",
|
|
17
|
-
"ack", "message", "request", "
|
|
17
|
+
"ack", "message", "request", "finish"]],
|
|
18
18
|
["Driven by adapters, not by people", ["attach", "heartbeat", "detach"]],
|
|
19
19
|
["About acc", ["help", "version", "update"]],
|
|
20
20
|
]);
|
|
@@ -33,10 +33,7 @@ const SUMMARY = Object.freeze({
|
|
|
33
33
|
message: "send a typed message to named participants",
|
|
34
34
|
inbox: "read only unresolved messages addressed to this participant",
|
|
35
35
|
reply: "reply to one message and acknowledge it in the same operation",
|
|
36
|
-
request: "ask another agent to do something
|
|
37
|
-
task: "create work, --take it, or move its --state along",
|
|
38
|
-
workstream: "group related work, and steer it with --take / --release",
|
|
39
|
-
decide: "record what was settled, so the next session does not reopen it",
|
|
36
|
+
request: "ask another agent to do something in a reply-required message",
|
|
40
37
|
finish: "write the handoff and release what this session held",
|
|
41
38
|
attach: "open a session; an adapter calls this, not a person",
|
|
42
39
|
heartbeat: "say the session is still alive",
|
|
@@ -6,7 +6,8 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
|
6
6
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
7
7
|
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
8
8
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
9
|
-
import { applyPlan, detectInstallation,
|
|
9
|
+
import { LIVE_POLICIES, applyPlan, describeActivation, detectInstallation, livePolicyOf,
|
|
10
|
+
loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
|
|
10
11
|
from "@agents-can-communicate/installer";
|
|
11
12
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
12
13
|
|
|
@@ -18,13 +19,18 @@ import { platformPaths } from "./platform-paths.mjs";
|
|
|
18
19
|
// Each client keeps its own directory under the user's home, and an adapter
|
|
19
20
|
// pointed at the home itself writes beside them rather than inside them. That
|
|
20
21
|
// install reports success and the client never reads a byte of it.
|
|
21
|
-
export const clientContext = (home, stateRoot) => ({
|
|
22
|
+
export const clientContext = (home, stateRoot, { shell = null, env = {} } = {}) => ({
|
|
22
23
|
home,
|
|
23
24
|
configDir: path.join(home, ".claude"),
|
|
24
25
|
agentsHome: home,
|
|
25
26
|
codexHome: path.join(home, ".codex"),
|
|
26
27
|
kimiHome: path.join(home, ".kimi-code"),
|
|
27
28
|
grokHome: path.join(home, ".grok"),
|
|
29
|
+
// The user's login shell and PATH, for the optional native shell bootstrap:
|
|
30
|
+
// which rc file could carry an ACC PATH block and which real executable a
|
|
31
|
+
// shim would exec. Detection reads them; nothing here writes.
|
|
32
|
+
shell,
|
|
33
|
+
env,
|
|
28
34
|
// Where ACC keeps its own state, for the client that has to be told. Codex
|
|
29
35
|
// sandboxes the commands a model runs to the workspace, and ACC's state is
|
|
30
36
|
// outside every workspace on purpose - so an agent there could read the
|
|
@@ -108,9 +114,20 @@ export function describeChanges(operation, home) {
|
|
|
108
114
|
return [...artifacts.filter(artifact => artifact.kind !== "merge")
|
|
109
115
|
.map(artifact => ` created ${shorten(artifact.path, home)}`), ...edited];
|
|
110
116
|
}
|
|
117
|
+
// An uninstall's merge edits come from the plan - every file ACC has ever
|
|
118
|
+
// written into - while everything else on this list is decided by the run.
|
|
119
|
+
// Printed unconditionally, a second uninstall told the operator it had edited
|
|
120
|
+
// six of their configuration files whose bytes it had not touched. `changes`
|
|
121
|
+
// is what the adapter actually took out, and is already what the packed
|
|
122
|
+
// verifier calls a no-op, so the report now follows the same definition. It
|
|
123
|
+
// is still the plan's list rather than a per-file one: this says whether
|
|
124
|
+
// anything was edited, not which of several files it was.
|
|
125
|
+
const editedIfAnything = (operation.changes ?? []).length === 0 ? [] : edited;
|
|
111
126
|
return [
|
|
112
127
|
...(operation.removed ?? []).map(file => ` removed ${shorten(file, home)}`),
|
|
113
|
-
...
|
|
128
|
+
...(operation.removedDirectories ?? [])
|
|
129
|
+
.map(file => ` removed ${shorten(file, home)}`),
|
|
130
|
+
...editedIfAnything,
|
|
114
131
|
...(operation.kept ?? [])
|
|
115
132
|
.map(file => ` kept ${shorten(file, home)} - changed since ACC wrote it`),
|
|
116
133
|
];
|
|
@@ -122,6 +139,9 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
122
139
|
+ (failed.length > 0 ? `; ${failed.length} failed` : ""),
|
|
123
140
|
...operations.filter(operation => operation.applied)
|
|
124
141
|
.flatMap(operation => describeChanges(operation, home)),
|
|
142
|
+
...operations.filter(operation => operation.applied
|
|
143
|
+
&& typeof operation.deliveryDiagnostic === "string")
|
|
144
|
+
.map(operation => ` ${operation.deliveryDiagnostic}`),
|
|
125
145
|
...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
|
|
126
146
|
...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`),
|
|
127
147
|
// Said once, where it is needed: the reader has just been shown a list of
|
|
@@ -165,7 +185,76 @@ export function failureOf({ action, acted, failed = [] }) {
|
|
|
165
185
|
export function actedOn(result) {
|
|
166
186
|
return result.operations.filter(operation => operation.applied
|
|
167
187
|
&& (result.action === "install"
|
|
168
|
-
|| (operation.removed?.length ?? 0) + (operation.
|
|
188
|
+
|| (operation.removed?.length ?? 0) + (operation.removedDirectories?.length ?? 0)
|
|
189
|
+
+ (operation.changes?.length ?? 0) > 0)).length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
193
|
+
&& runtime.isInteractive() === true;
|
|
194
|
+
|
|
195
|
+
// One default-No question per eligible client, asked only after detection has
|
|
196
|
+
// finished and only when a person is at both ends of the terminal. It names
|
|
197
|
+
// the client, the mechanism, and the files or service it would touch, and says
|
|
198
|
+
// when the choice takes effect.
|
|
199
|
+
function questionFor(entry, context) {
|
|
200
|
+
const activation = { livePolicy: "actionable", shell: context.shell,
|
|
201
|
+
rcFile: rcFileFor(context.home, context.shell), shimDir: shimDirFor(context.stateRoot),
|
|
202
|
+
mechanisms: entry.nativeDelivery.activationPlan.mechanisms };
|
|
203
|
+
return [`Enable native live delivery for ${entry.displayName} ${entry.version}?`,
|
|
204
|
+
...describeActivation(activation).map(line => ` ${line}`),
|
|
205
|
+
" applies to newly started sessions; a new PATH block needs a new or reloaded shell"]
|
|
206
|
+
.join("\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Which live policy each selected client gets.
|
|
211
|
+
*
|
|
212
|
+
* An explicit --delivery applies uniformly and never prompts. Otherwise a
|
|
213
|
+
* recorded opt-in is kept as it was consented to, a fresh or never-activated
|
|
214
|
+
* client is off unless a person answers yes here, and a dry run or a
|
|
215
|
+
* non-interactive stdin/stdout makes no interactive choice at all.
|
|
216
|
+
*/
|
|
217
|
+
export async function decideDelivery({ options, detected, recorded, runtime, dryRun, context }) {
|
|
218
|
+
const explicit = options.delivery;
|
|
219
|
+
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
220
|
+
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
221
|
+
}
|
|
222
|
+
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
223
|
+
const deliveryByAdapter = {};
|
|
224
|
+
const asked = [];
|
|
225
|
+
let withheld = 0;
|
|
226
|
+
for (const entry of detected) {
|
|
227
|
+
const eligible = entry.nativeDelivery?.state === "eligible";
|
|
228
|
+
const previous = livePolicyOf(recordedById.get(entry.adapterId));
|
|
229
|
+
if (explicit !== undefined) {
|
|
230
|
+
deliveryByAdapter[entry.adapterId] = explicit;
|
|
231
|
+
} else if (previous !== "off") {
|
|
232
|
+
deliveryByAdapter[entry.adapterId] = previous;
|
|
233
|
+
} else if (!eligible || dryRun || !isInteractive(runtime)) {
|
|
234
|
+
deliveryByAdapter[entry.adapterId] = "off";
|
|
235
|
+
if (eligible) withheld += 1;
|
|
236
|
+
} else {
|
|
237
|
+
const yes = await runtime.confirm(questionFor(entry, context),
|
|
238
|
+
{ input: runtime.input, output: runtime.output }) === true;
|
|
239
|
+
deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
|
|
240
|
+
asked.push(entry.adapterId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const notes = explicit === undefined && dryRun && withheld > 0
|
|
244
|
+
? ["interactive choices were not made: this preview keeps native delivery off for "
|
|
245
|
+
+ `${withheld} eligible client(s) without a recorded opt-in`]
|
|
246
|
+
: [];
|
|
247
|
+
return { deliveryByAdapter, asked, notes };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Said once, after the first PATH block is written: a running shell and a
|
|
251
|
+
// running client know nothing about it.
|
|
252
|
+
function reloadAdvice(operations) {
|
|
253
|
+
const appended = operations.filter(operation => operation.appendedRcBlock === true);
|
|
254
|
+
if (appended.length === 0) return [];
|
|
255
|
+
const rc = appended[0].nativeActivation?.rcFile ?? "your shell rc file";
|
|
256
|
+
return ["", `native delivery is wired for new sessions: open a new terminal (or reload ${rc}), `
|
|
257
|
+
+ "then start the client normally; a session already running keeps durable delivery"];
|
|
169
258
|
}
|
|
170
259
|
|
|
171
260
|
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
@@ -173,7 +262,8 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
173
262
|
const home = options.home ?? runtime.env?.HOME ?? homedir();
|
|
174
263
|
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
175
264
|
env: runtime.env ?? {} });
|
|
176
|
-
const context = clientContext(home, path.join(dataHome, "acc")
|
|
265
|
+
const context = clientContext(home, path.join(dataHome, "acc"),
|
|
266
|
+
{ shell: shellOf(runtime.env), env: runtime.env ?? {} });
|
|
177
267
|
|
|
178
268
|
const detected = await detectInstallation({ adapters, context,
|
|
179
269
|
probeTimeoutMs: probeTimeout(runtime.env) });
|
|
@@ -199,19 +289,28 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
199
289
|
const requested = options.adapter === undefined
|
|
200
290
|
? []
|
|
201
291
|
: (Array.isArray(options.adapter) ? options.adapter : [options.adapter]);
|
|
292
|
+
// Decided only after detection, from the operator's explicit answer, the
|
|
293
|
+
// recorded consent, or a default of off; never from inference.
|
|
294
|
+
const decided = action === "install"
|
|
295
|
+
? await decideDelivery({ options, detected, recorded, runtime, dryRun, context })
|
|
296
|
+
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
202
297
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
203
|
-
accVersion, allowDowngrade: options.downgrade === true, requested
|
|
298
|
+
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
299
|
+
deliveryByAdapter: decided.deliveryByAdapter });
|
|
204
300
|
const result = await applyPlan({ plan, adapters, context, dataHome, dryRun, accVersion });
|
|
205
301
|
|
|
206
302
|
const acted = actedOn(result);
|
|
207
303
|
if (dryRun) {
|
|
208
|
-
return { data: { ...result, plan, dataHome },
|
|
304
|
+
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter },
|
|
209
305
|
text: [`would ${action}:`, ...plan.operations.flatMap(operation => operation.summary),
|
|
210
|
-
...plan.skipped.map(entry => `skip ${entry.adapterId}: ${entry.reason}`)
|
|
306
|
+
...plan.skipped.map(entry => `skip ${entry.adapterId}: ${entry.reason}`),
|
|
307
|
+
...decided.notes].join("\n") };
|
|
211
308
|
}
|
|
212
309
|
|
|
213
|
-
return { data: { ...result, plan, dataHome
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
310
|
+
return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
|
|
311
|
+
asked: decided.asked },
|
|
312
|
+
text: [describeOutcome({ action, acted, failed: result.failed,
|
|
313
|
+
skipped: plan.skipped, operations: result.operations, home }),
|
|
314
|
+
...reloadAdvice(result.operations)].join("\n"),
|
|
315
|
+
error: failureOf({ action, acted, failed: result.failed }) };
|
|
217
316
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
// Kept cohesive above 300 lines because this is the single CLI composition boundary: every
|
|
2
|
+
// handler shares one owner-resolution, result, and error contract with `main`. Splitting the
|
|
3
|
+
// dispatch table would either duplicate that contract or expose storage/runtime context solely
|
|
4
|
+
// to pass it across a module boundary, making command behavior harder to audit as one surface.
|
|
5
|
+
import { AccError, CONFIG_FILENAME, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS,
|
|
6
|
+
failure, ok }
|
|
2
7
|
from "@agents-can-communicate/protocol";
|
|
3
|
-
import { createCoordinationService
|
|
8
|
+
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
4
9
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
5
10
|
|
|
6
11
|
import { parseArgs, positiveNumber } from "./args.mjs";
|
|
@@ -80,8 +85,11 @@ async function locateContext(options, runtime) {
|
|
|
80
85
|
async function withService({ descriptor, paths }, runtime) {
|
|
81
86
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
82
87
|
ids: runtime.ids, workspaceId: descriptor.id });
|
|
88
|
+
const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
83
89
|
return { descriptor, paths,
|
|
84
|
-
service
|
|
90
|
+
service,
|
|
91
|
+
deliveryRouter: typeof runtime.createDeliveryRouter === "function"
|
|
92
|
+
? runtime.createDeliveryRouter({ service, clock: runtime.clock }) : null };
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
async function openContext(options, runtime) {
|
|
@@ -107,6 +115,49 @@ async function openDiagnosticContext(options, runtime) {
|
|
|
107
115
|
|
|
108
116
|
const human = value => (typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
109
117
|
|
|
118
|
+
const clientMessageId = (options, context) =>
|
|
119
|
+
options.clientMessageId ?? context.service.ids.next("client");
|
|
120
|
+
|
|
121
|
+
function obligationFor(kind, explicit, addressed) {
|
|
122
|
+
if (!GENERIC_MESSAGE_KINDS.includes(kind)) {
|
|
123
|
+
const command = kind === "answer" ? "reply" : kind === "handoff" ? "finish" : "message";
|
|
124
|
+
throw usage(kind === "answer" || kind === "handoff"
|
|
125
|
+
? `${kind} messages require acc ${command}` : `unknown message type: ${kind}`);
|
|
126
|
+
}
|
|
127
|
+
const obligation = explicit ?? VALID_OBLIGATIONS[kind][0];
|
|
128
|
+
if (!VALID_OBLIGATIONS[kind].includes(obligation)
|
|
129
|
+
|| (!addressed && obligation !== "none")) {
|
|
130
|
+
throw usage(`message obligation ${obligation} is invalid for ${kind}`);
|
|
131
|
+
}
|
|
132
|
+
return obligation;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function recordedText(message, delivery) {
|
|
136
|
+
const diagnostics = delivery.map(item => item.outcome === "offered"
|
|
137
|
+
? `live offered to ${item.recipientParticipantId} via ${item.transport}`
|
|
138
|
+
: item.outcome === "queued"
|
|
139
|
+
? `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
|
|
140
|
+
: `delivery already ${item.outcome}`);
|
|
141
|
+
return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function recordAndOffer({ record, router, selectMessage = value => value }) {
|
|
145
|
+
const recorded = await record();
|
|
146
|
+
const message = selectMessage(recorded);
|
|
147
|
+
if (router === null || router === undefined
|
|
148
|
+
|| !Array.isArray(message?.toParticipantIds) || message.toParticipantIds.length === 0) {
|
|
149
|
+
return { recorded, delivery: [] };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return { recorded, delivery: await router.offer(message) };
|
|
153
|
+
} catch {
|
|
154
|
+
return { recorded, delivery: message.toParticipantIds.map(recipientParticipantId => ({
|
|
155
|
+
recipientParticipantId, outcome: "queued", transport: "durable",
|
|
156
|
+
errorCode: "transport_error",
|
|
157
|
+
})) };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
110
161
|
/** How many are here, and how many only look it. */
|
|
111
162
|
export function describePresence({ live = 0, stale = 0 } = {}) {
|
|
112
163
|
if (live === 0) return "0 live";
|
|
@@ -168,8 +219,7 @@ const HANDLERS = Object.freeze({
|
|
|
168
219
|
if (options.summary === undefined) throw usage("work requires --summary");
|
|
169
220
|
const intent = await context.service.setIntent({ sessionId: options.session,
|
|
170
221
|
generation: options.generation, summary: options.summary, mode: options.mode ?? "edit",
|
|
171
|
-
state: options.state,
|
|
172
|
-
resourceHints: options.hint ?? [] });
|
|
222
|
+
state: options.state, resourceHints: options.hint ?? [] });
|
|
173
223
|
return { data: intent, text: `intent: ${intent.summary}` };
|
|
174
224
|
},
|
|
175
225
|
|
|
@@ -194,18 +244,17 @@ const HANDLERS = Object.freeze({
|
|
|
194
244
|
},
|
|
195
245
|
|
|
196
246
|
message: async ({ options, context }) => {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
text: advice ? `sent ${message.messageId}\n${advice}` : `sent ${message.messageId}` };
|
|
247
|
+
const kind = options.type ?? "note";
|
|
248
|
+
const toParticipantIds = options.to ?? [];
|
|
249
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter, record: () =>
|
|
250
|
+
context.service.sendMessage({ sessionId: options.session,
|
|
251
|
+
generation: options.generation, clientMessageId: clientMessageId(options, context),
|
|
252
|
+
toParticipantIds, kind,
|
|
253
|
+
obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0),
|
|
254
|
+
subject: options.subject, body: options.body, descriptor: context.descriptor }) });
|
|
255
|
+
const message = routed.recorded;
|
|
256
|
+
return { data: { message, delivery: routed.delivery },
|
|
257
|
+
text: recordedText(message, routed.delivery) };
|
|
209
258
|
},
|
|
210
259
|
|
|
211
260
|
inbox: async ({ options, context }) => {
|
|
@@ -216,121 +265,51 @@ const HANDLERS = Object.freeze({
|
|
|
216
265
|
},
|
|
217
266
|
|
|
218
267
|
reply: async ({ options, context }) => {
|
|
219
|
-
const
|
|
268
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter,
|
|
269
|
+
selectMessage: value => value.reply, record: () => context.service.replyToMessage({
|
|
270
|
+
sessionId: options.session,
|
|
220
271
|
generation: options.generation, messageId: options.message, body: options.body,
|
|
221
|
-
subject: options.subject,
|
|
222
|
-
|
|
223
|
-
|
|
272
|
+
subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
|
|
273
|
+
const result = routed.recorded;
|
|
274
|
+
return { data: { message: result.reply, delivery: routed.delivery },
|
|
275
|
+
text: recordedText(result.reply, routed.delivery) };
|
|
224
276
|
},
|
|
225
277
|
|
|
226
|
-
/**
|
|
227
|
-
* Ask another agent to do something. One call, one write.
|
|
228
|
-
*
|
|
229
|
-
* The recipient hears about it twice by design: the task raises an attention
|
|
230
|
-
* item for the participant it names, and the message reaches their turn as
|
|
231
|
-
* quoted peer text explaining why.
|
|
232
|
-
*/
|
|
233
278
|
request: async ({ options, context }) => {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
279
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter, record: () =>
|
|
280
|
+
context.service.sendMessage({
|
|
281
|
+
sessionId: options.session,
|
|
282
|
+
generation: options.generation,
|
|
283
|
+
clientMessageId: clientMessageId(options, context),
|
|
284
|
+
toParticipantIds: [options.to],
|
|
285
|
+
kind: "request",
|
|
286
|
+
obligation: "reply",
|
|
287
|
+
subject: options.title,
|
|
288
|
+
body: options.detail ?? options.title,
|
|
289
|
+
descriptor: context.descriptor,
|
|
290
|
+
}) });
|
|
291
|
+
const message = routed.recorded;
|
|
292
|
+
return { data: { message, delivery: routed.delivery },
|
|
293
|
+
text: recordedText(message, routed.delivery) };
|
|
241
294
|
},
|
|
242
295
|
|
|
243
296
|
ack: async ({ options, context }) => {
|
|
244
|
-
const receipt = await context.service.
|
|
245
|
-
generation: options.generation, messageId: options.message
|
|
246
|
-
state: options.state ?? "acknowledged" });
|
|
297
|
+
const receipt = await context.service.acknowledgeMessage({ sessionId: options.session,
|
|
298
|
+
generation: options.generation, messageId: options.message });
|
|
247
299
|
return { data: receipt, text: `${receipt.messageId} ${receipt.state}` };
|
|
248
300
|
},
|
|
249
301
|
|
|
250
|
-
/**
|
|
251
|
-
* What was settled, and on whose authority.
|
|
252
|
-
*
|
|
253
|
-
* `--human` is the caller stating that a person actually decided this. The
|
|
254
|
-
* core refuses `--authority human` without it, because a peer proposal
|
|
255
|
-
* becoming a human decision on its own is the one way this record could
|
|
256
|
-
* launder an agent's opinion into a ruling.
|
|
257
|
-
*/
|
|
258
|
-
decide: async ({ options, context }) => {
|
|
259
|
-
const decision = await context.service.recordDecision({
|
|
260
|
-
sessionId: options.session, generation: options.generation,
|
|
261
|
-
title: options.title, outcome: options.outcome,
|
|
262
|
-
authority: options.authority ?? "workstream",
|
|
263
|
-
workstreamId: options.workstream ?? null,
|
|
264
|
-
decidedBy: options.decidedBy,
|
|
265
|
-
supersedes: options.supersedes ?? null,
|
|
266
|
-
humanConfirmed: options.human === true,
|
|
267
|
-
descriptor: context.descriptor });
|
|
268
|
-
return { data: decision,
|
|
269
|
-
text: `${decision.decisionId} ${decision.authority}: ${decision.title}` };
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
workstream: async ({ options, context }) => {
|
|
273
|
-
const owner = { sessionId: options.session, generation: options.generation };
|
|
274
|
-
// Taking the coordination of one and creating one are the same noun, so
|
|
275
|
-
// they stay one command rather than two the model has to choose between -
|
|
276
|
-
// the same shape `acc task` already has.
|
|
277
|
-
if (options.take === true || options.release === true) {
|
|
278
|
-
if (options.workstream === undefined) {
|
|
279
|
-
throw usage(`workstream --${options.take === true ? "take" : "release"} `
|
|
280
|
-
+ "requires --workstream");
|
|
281
|
-
}
|
|
282
|
-
const acted = options.take === true
|
|
283
|
-
? await context.service.acquireCoordinator({ ...owner,
|
|
284
|
-
workstreamId: options.workstream })
|
|
285
|
-
: await context.service.releaseCoordinator({ ...owner,
|
|
286
|
-
workstreamId: options.workstream });
|
|
287
|
-
return { data: acted,
|
|
288
|
-
text: `${acted.workstreamId} ${options.take === true ? "coordinated" : "released"}` };
|
|
289
|
-
}
|
|
290
|
-
if (options.title === undefined) throw usage("workstream requires --title");
|
|
291
|
-
if (options.objective === undefined) throw usage("workstream requires --objective");
|
|
292
|
-
const workstream = await context.service.createWorkstream({ ...owner,
|
|
293
|
-
title: options.title, objective: options.objective,
|
|
294
|
-
descriptor: context.descriptor });
|
|
295
|
-
return { data: workstream, text: `${workstream.workstreamId} ${workstream.state}` };
|
|
296
|
-
},
|
|
297
|
-
|
|
298
|
-
task: async ({ options, context }) => {
|
|
299
|
-
const owner = { sessionId: options.session, generation: options.generation };
|
|
300
|
-
// Taking work and moving it along are the same noun as creating it, so they
|
|
301
|
-
// stay one command rather than three the model has to choose between.
|
|
302
|
-
if (options.take === true) {
|
|
303
|
-
if (options.task === undefined) throw usage("task --take requires --task");
|
|
304
|
-
const taken = await context.service.claimTask({ ...owner, taskId: options.task,
|
|
305
|
-
force: options.force === true });
|
|
306
|
-
return { data: taken, text: `${taken.taskId} ${taken.state}` };
|
|
307
|
-
}
|
|
308
|
-
if (options.decline === true) {
|
|
309
|
-
if (options.task === undefined) throw usage("task --decline requires --task");
|
|
310
|
-
const refused = await context.service.declineTask({ ...owner,
|
|
311
|
-
taskId: options.task, reason: options.reason });
|
|
312
|
-
return { data: refused, text: `${refused.taskId} declined` };
|
|
313
|
-
}
|
|
314
|
-
if (options.state !== undefined) {
|
|
315
|
-
if (options.task === undefined) throw usage("task --state requires --task");
|
|
316
|
-
const moved = await context.service.transitionTask({ ...owner,
|
|
317
|
-
taskId: options.task, state: options.state });
|
|
318
|
-
return { data: moved, text: `${moved.taskId} ${moved.state}` };
|
|
319
|
-
}
|
|
320
|
-
if (options.title === undefined) throw usage("task requires --title");
|
|
321
|
-
const task = await context.service.createTask({ ...owner,
|
|
322
|
-
workstreamId: options.workstream, title: options.title, detail: options.detail,
|
|
323
|
-
assigneeParticipantId: options.assignee, taskId: options.task,
|
|
324
|
-
dependsOn: options.dependsOn ?? [], descriptor: context.descriptor });
|
|
325
|
-
return { data: task, text: `${task.taskId} ${task.state}` };
|
|
326
|
-
},
|
|
327
|
-
|
|
328
302
|
finish: async ({ options, context }) => {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
303
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter,
|
|
304
|
+
selectMessage: value => value.message, record: () => context.service.finishSession({
|
|
305
|
+
sessionId: options.session,
|
|
306
|
+
generation: options.generation, clientMessageId: clientMessageId(options, context),
|
|
307
|
+
goal: options.goal, status: options.status,
|
|
308
|
+
toParticipantId: options.to, completed: options.completed ?? [],
|
|
309
|
+
remaining: options.remaining ?? [], blockers: options.blocker ?? [] }) });
|
|
310
|
+
const handoff = routed.recorded;
|
|
311
|
+
return { data: { message: handoff.message, delivery: routed.delivery },
|
|
312
|
+
text: recordedText(handoff.message, routed.delivery) };
|
|
334
313
|
},
|
|
335
314
|
|
|
336
315
|
status: async ({ options, context }) => {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
import { overlaps } from "./claims.mjs";
|
|
4
|
+
import { classifySessionPresence } from "./sessions.mjs";
|
|
5
|
+
|
|
6
|
+
export const ATTENTION_PRIORITY = Object.freeze({
|
|
7
|
+
reply_required: 1,
|
|
8
|
+
acknowledgement_required: 2,
|
|
9
|
+
recipient_unavailable: 3,
|
|
10
|
+
claim_conflict: 4,
|
|
11
|
+
claim_contended: 5,
|
|
12
|
+
claim_expired: 6,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function obligationItems(snapshot, participantId) {
|
|
16
|
+
const messages = new Map((snapshot.messages ?? []).map(item => [item.messageId, item]));
|
|
17
|
+
return (snapshot.receipts ?? []).flatMap(receipt => {
|
|
18
|
+
if (receipt.recipientParticipantId !== participantId
|
|
19
|
+
|| receipt.state === "acknowledged") return [];
|
|
20
|
+
const message = messages.get(receipt.messageId);
|
|
21
|
+
const kind = message?.obligation === "reply" ? "reply_required"
|
|
22
|
+
: message?.obligation === "acknowledge" ? "acknowledgement_required" : null;
|
|
23
|
+
const action = kind === "reply_required" ? "a reply" : "acknowledgement";
|
|
24
|
+
return kind === null ? [] : [{ kind, priority: ATTENTION_PRIORITY[kind],
|
|
25
|
+
sourceId: message.messageId,
|
|
26
|
+
summary: `message ${message.messageId} from ${message.fromParticipantId} is a `
|
|
27
|
+
+ `${message.kind} requiring ${action}` }];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function presenceByParticipant(snapshot, now, pidIsAlive) {
|
|
32
|
+
const online = new Set();
|
|
33
|
+
for (const session of snapshot.sessions ?? []) {
|
|
34
|
+
if (classifySessionPresence(session, now, pidIsAlive) === "online") {
|
|
35
|
+
online.add(session.participantId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return online;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unavailableRecipients(snapshot, participantId, now, pidIsAlive) {
|
|
42
|
+
const online = presenceByParticipant(snapshot, now, pidIsAlive);
|
|
43
|
+
const receipts = snapshot.receipts ?? [];
|
|
44
|
+
return (snapshot.messages ?? []).flatMap(message => {
|
|
45
|
+
if (message.fromParticipantId !== participantId || message.toParticipantIds?.length === 0
|
|
46
|
+
|| message.obligation === "none") return [];
|
|
47
|
+
return receipts.filter(receipt => receipt.messageId === message.messageId
|
|
48
|
+
&& receipt.state !== "acknowledged"
|
|
49
|
+
&& !online.has(receipt.recipientParticipantId))
|
|
50
|
+
.map(receipt => ({ kind: "recipient_unavailable",
|
|
51
|
+
priority: ATTENTION_PRIORITY.recipient_unavailable,
|
|
52
|
+
sourceId: message.messageId,
|
|
53
|
+
summary: `${message.subject} - ${receipt.recipientParticipantId} is unavailable` }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function expiredClaims(snapshot, session, now) {
|
|
58
|
+
if (session == null) return [];
|
|
59
|
+
return (snapshot.claims ?? []).filter(claim => claim.ownerSessionId === session.sessionId
|
|
60
|
+
&& Date.parse(claim.expiresAt) <= Date.parse(now))
|
|
61
|
+
.map(claim => ({ kind: "claim_expired", priority: ATTENTION_PRIORITY.claim_expired,
|
|
62
|
+
sourceId: claim.claimId,
|
|
63
|
+
summary: `${claim.resource} - your claim has run out, and peers can write to it` }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function claimConflicts(snapshot, session, now) {
|
|
67
|
+
const mine = (snapshot.intents ?? []).find(intent => intent.sessionId === session?.sessionId);
|
|
68
|
+
if (mine === undefined) return [];
|
|
69
|
+
return (snapshot.claims ?? []).filter(claim => claim.ownerSessionId !== session.sessionId
|
|
70
|
+
&& Date.parse(claim.expiresAt) > Date.parse(now)
|
|
71
|
+
&& mine.resourceHints.some(hint => overlaps(hint, claim.resource)))
|
|
72
|
+
.map(claim => ({ kind: "claim_conflict", priority: ATTENTION_PRIORITY.claim_conflict,
|
|
73
|
+
sourceId: claim.claimId,
|
|
74
|
+
summary: `${claim.resource} is claimed by ${claim.ownerSessionId}` }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function claimContended(snapshot, session, now) {
|
|
78
|
+
if (session == null) return [];
|
|
79
|
+
const peerIntents = (snapshot.intents ?? [])
|
|
80
|
+
.filter(intent => intent.sessionId !== session.sessionId);
|
|
81
|
+
return (snapshot.claims ?? []).filter(claim => claim.ownerSessionId === session.sessionId
|
|
82
|
+
&& Date.parse(claim.expiresAt) > Date.parse(now)).flatMap(claim => {
|
|
83
|
+
const peer = peerIntents.find(intent => intent.resourceHints
|
|
84
|
+
.some(hint => overlaps(hint, claim.resource)));
|
|
85
|
+
if (peer === undefined) return [];
|
|
86
|
+
const participant = (snapshot.sessions ?? [])
|
|
87
|
+
.find(item => item.sessionId === peer.sessionId)?.participantId ?? "a peer";
|
|
88
|
+
return [{ kind: "claim_contended", priority: ATTENTION_PRIORITY.claim_contended,
|
|
89
|
+
sourceId: claim.claimId,
|
|
90
|
+
summary: `${claim.resource} - ${participant} means to work on what you hold` }];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function computeAttention(snapshot, { session, participantId, now, pidIsAlive }) {
|
|
95
|
+
if (typeof pidIsAlive !== "function") {
|
|
96
|
+
throw new AccError(EXIT.USAGE, "computeAttention requires a pidIsAlive probe", {});
|
|
97
|
+
}
|
|
98
|
+
return [
|
|
99
|
+
...obligationItems(snapshot, participantId),
|
|
100
|
+
...unavailableRecipients(snapshot, participantId, now, pidIsAlive),
|
|
101
|
+
...claimConflicts(snapshot, session, now),
|
|
102
|
+
...claimContended(snapshot, session, now),
|
|
103
|
+
...expiredClaims(snapshot, session, now),
|
|
104
|
+
].sort((left, right) => left.priority - right.priority
|
|
105
|
+
|| left.sourceId.localeCompare(right.sourceId));
|
|
106
|
+
}
|