agents-can-communicate 0.10.4 → 0.11.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.
Files changed (98) hide show
  1. package/README.md +29 -21
  2. package/bin/acc-indicator.mjs +5 -0
  3. package/bin/entrypoints/acc-antigravity-relay.mjs +2 -2
  4. package/bin/entrypoints/acc-hook.mjs +4 -24
  5. package/bin/entrypoints/acc-mcp.mjs +8 -6
  6. package/bin/entrypoints/acc.mjs +22 -11
  7. package/bin/entrypoints/antigravity-relay-binding.mjs +1 -1
  8. package/docs/CAPABILITIES.md +3 -0
  9. package/docs/CLI.md +34 -7
  10. package/docs/MCP.md +42 -2
  11. package/docs/PROTOCOL.md +19 -0
  12. package/docs/STATUS_INDICATOR.md +133 -0
  13. package/docs/TROUBLESHOOTING.md +2 -2
  14. package/docs/WHY_ACC.md +14 -8
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +29 -5
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +83 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
  27. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  28. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +29 -5
  29. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  30. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +29 -5
  31. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  32. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  33. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +29 -5
  34. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +4 -0
  35. package/node_modules/@agents-can-communicate/adapter-grok/src/indicator-install.mjs +30 -0
  36. package/node_modules/@agents-can-communicate/adapter-grok/src/indicator.mjs +23 -0
  37. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +9 -5
  38. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +29 -5
  40. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +4 -0
  41. package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator-install.mjs +30 -0
  42. package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator.mjs +17 -0
  43. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +12 -6
  44. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  45. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
  46. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +5 -1
  47. package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-command.mjs +14 -0
  48. package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-text.mjs +13 -0
  49. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt-writer.mjs +1 -1
  50. package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-order.mjs +12 -0
  51. package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
  52. package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +40 -27
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
  56. package/node_modules/@agents-can-communicate/cli/package.json +8 -1
  57. package/node_modules/@agents-can-communicate/cli/src/adapter-catalog.mjs +33 -0
  58. package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
  59. package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
  60. package/node_modules/@agents-can-communicate/cli/src/help.mjs +17 -11
  61. package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
  62. package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
  63. package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
  64. package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
  65. package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
  66. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
  67. package/node_modules/@agents-can-communicate/cli/src/main.mjs +38 -110
  68. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +19 -0
  69. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  70. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +1 -1
  71. package/node_modules/@agents-can-communicate/cli/src/recorded-text.mjs +32 -0
  72. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  73. package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
  74. package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
  75. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +2 -1
  76. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +5 -2
  77. package/node_modules/@agents-can-communicate/core/src/sync.mjs +2 -1
  78. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  79. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -7
  80. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  81. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +23 -11
  82. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +66 -26
  83. package/node_modules/@agents-can-communicate/installer/package.json +3 -2
  84. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -0
  85. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
  86. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
  87. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  88. package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
  89. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
  90. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
  91. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
  92. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  93. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
  94. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +3 -2
  95. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
  96. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
  97. package/node_modules/@agents-can-communicate/storage-filesystem/src/state-reads.mjs +24 -7
  98. package/package.json +3 -1
@@ -0,0 +1,112 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { assertPortableId } from "@agents-can-communicate/protocol";
4
+ import { effectiveCapabilities, loadNativeAttempt, loadSessionBinding, loadOfferObservation }
5
+ from "@agents-can-communicate/adapter-sdk";
6
+ import { readDeliveryBindingRecord, readSessionRecord } from "@agents-can-communicate/storage-filesystem";
7
+ import { readInstalledLivePolicyState } from "@agents-can-communicate/installer/live-policy";
8
+ import { loadAdapter } from "./adapter-catalog.mjs";
9
+ import { canonicalManagerRoot, managedDirectory, readManagedJson } from "./managed-runtime/state.mjs";
10
+ import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
11
+
12
+ const receptionText = {
13
+ automatic: "This chat can receive addressed requests without another user prompt. A busy client may queue them.",
14
+ turn: "Messages reach this chat on its next normal turn.",
15
+ inbox: "Messages are available when the agent reads acc inbox; no polling schedule is implied.",
16
+ };
17
+
18
+ function result({ health = "ready", reception = null, reasonCode = null,
19
+ detail, action = null, leaseCurrent = null }) {
20
+ const qualifier = reception && reception !== "automatic" ? ` · ${reception}` : "";
21
+ return { health, reception, reasonCode, leaseCurrent,
22
+ label: `ACC ${health === "ready" ? "●" : "!"}${qualifier}`,
23
+ detail: [detail, receptionText[reception]].filter(Boolean).join(" "), action };
24
+ }
25
+
26
+ export const indicatorFailure = (reasonCode = "state_unreadable") => result({
27
+ health: "problem", reasonCode,
28
+ detail: reasonCode === "not_registered" ? "This chat is not registered with ACC."
29
+ : "ACC could not read this chat's local integration state.",
30
+ action: reasonCode === "not_registered"
31
+ ? "Send a prompt to run the session hooks. If ACC is still absent, run acc doctor and check the plugin is enabled."
32
+ : "Run acc doctor in this project to identify and repair the local state problem.",
33
+ });
34
+
35
+ const actions = {
36
+ client_session_embedded: "This session has no shared delivery service. Run acc doctor to see the launch option and how to open a supported session.",
37
+ client_process_unknown: "Send a prompt: each prompt looks for the client process again. If this stays, run acc doctor.",
38
+ transport_permission_denied: "Allow the sending command to access the local delivery channel; acc doctor shows the affected session.",
39
+ handshake_version_mismatch: "Update the client's delivery service to a supported version; acc doctor shows the version in use.",
40
+ native_policy_off: "Automatic delivery is disabled in ACC settings. Run acc install --delivery actionable to enable it.",
41
+ inbound_approval_required: "Approve the pending inbound request in the receiving client, or review its incoming-message setting.",
42
+ };
43
+
44
+ /** A display observation, never CLI ownership. Native ids select one saved room
45
+ * and one binding; there is no roster scan, Git probe, store open, socket probe,
46
+ * lease refresh, heartbeat, update worker or managed-runtime process lease. */
47
+ export async function readIndicator({ adapterId, nativeSessionId,
48
+ dataHome = platformDataHome(), now = new Date().toISOString() }) {
49
+ try {
50
+ const adapter = await loadAdapter(adapterId);
51
+ if (!adapter || typeof nativeSessionId !== "string" || !nativeSessionId
52
+ || nativeSessionId.length > 4096) return indicatorFailure("not_registered");
53
+ const key = createHash("sha256").update(JSON.stringify([adapterId, nativeSessionId])).digest("hex");
54
+ const accRoot = await canonicalManagerRoot(path.join(dataHome, "acc"));
55
+ const rooms = path.join(accRoot, "native-workspaces");
56
+ if (!await managedDirectory(rooms)) return indicatorFailure("not_registered");
57
+ const room = await readManagedJson(path.join(rooms, `${key}.json`));
58
+ if (!room) return indicatorFailure("not_registered");
59
+ if (room.schemaVersion !== 1 || room.adapterId !== adapterId
60
+ || room.nativeSessionId !== nativeSessionId) throw new Error("invalid room");
61
+ assertPortableId(room.workspaceId, "workspace id");
62
+ const root = runtimePaths({ dataHome: path.dirname(accRoot), workspaceId: room.workspaceId }).root;
63
+ const owner = await loadSessionBinding({ runtimeDir: root, harnessSessionId: nativeSessionId });
64
+ if (!owner) return indicatorFailure("not_registered");
65
+ const input = { root, workspaceId: room.workspaceId, sessionId: owner.accSessionId };
66
+ const session = await readSessionRecord(input);
67
+ if (session?.state !== "open" || session.generation !== owner.generation
68
+ || session.harness !== adapterId) return indicatorFailure("not_registered");
69
+ const [binding, handshake, policy, offer] = await Promise.all([
70
+ readDeliveryBindingRecord(input),
71
+ loadNativeAttempt({ runtimeDir: root, harnessSessionId: nativeSessionId, ...owner }),
72
+ readInstalledLivePolicyState({ dataHome: path.dirname(accRoot), adapterId }),
73
+ loadOfferObservation({ runtimeDir: root, sessionId: owner.accSessionId, generation: owner.generation }),
74
+ ]);
75
+ const attempt = offer && (!handshake || Date.parse(offer.at) >= Date.parse(handshake.at)) ? offer : handshake;
76
+ const current = await readSessionRecord(input);
77
+ if (current?.state !== "open" || current.generation !== owner.generation) {
78
+ return indicatorFailure("not_registered");
79
+ }
80
+ const capabilities = effectiveCapabilities(adapter, {
81
+ clientName: owner.clientName ?? adapter.client.certificationName,
82
+ clientVersion: owner.clientVersion, platform: owner.platform,
83
+ });
84
+ const fallback = capabilities.delivery.nextTurn ? "turn" : "inbox";
85
+ const bound = binding?.generation === owner.generation && binding.adapterId === adapterId
86
+ && binding.retiredAt == null && binding.availableModes.includes("livePush");
87
+ const leaseCurrent = bound ? Date.parse(binding.leaseUntil) > Date.parse(now) : null;
88
+ if (["missing", "invalid", "unavailable"].includes(policy.policyStatus)) {
89
+ return result({ health: "problem", reception: fallback, reasonCode: "installation_unavailable",
90
+ detail: "ACC cannot find a valid delivery setting for this integration.",
91
+ action: `Run acc install --adapter ${adapterId} to repair the integration.`, leaseCurrent });
92
+ }
93
+ if (policy.policy === "off") return result({ reception: fallback, reasonCode: "native_policy_off",
94
+ detail: "Automatic delivery is disabled in ACC settings.", leaseCurrent });
95
+ if (attempt?.state === "degraded") return result({ health: "problem", reception: fallback,
96
+ reasonCode: attempt.reasonCode ?? "handshake_failed", leaseCurrent,
97
+ detail: "ACC could not establish automatic delivery for this chat.",
98
+ action: actions[attempt.reasonCode]
99
+ ?? "Run acc doctor in this project for the channel failure and the client's recovery steps.",
100
+ });
101
+ // Lease expiry alone is neither disconnection nor an operation in progress.
102
+ // The delivery router verifies the endpoint and renews it on the next send.
103
+ if (bound && binding.livePolicy !== "off") return result({ reception: "automatic", leaseCurrent });
104
+ if (attempt?.state === "unsupported" || !capabilities.delivery.livePush) {
105
+ return result({ reception: fallback, reasonCode: "native_delivery_unsupported",
106
+ detail: "This client session does not support automatic wake-up." });
107
+ }
108
+ return result({ health: "problem", reception: fallback, reasonCode: "native_unbound",
109
+ detail: "This chat has no automatic delivery channel.",
110
+ action: "Send a prompt to let ACC establish the channel; if it remains unavailable, run acc doctor." });
111
+ } catch { return indicatorFailure(); }
112
+ }
@@ -224,6 +224,9 @@ export function actedOn(result) {
224
224
  }
225
225
 
226
226
  export async function runInstallCommand({ options, runtime, action = "install" }) {
227
+ if (options.indicator !== undefined && !["on", "off"].includes(options.indicator)) {
228
+ throw new AccError(EXIT.USAGE, "--indicator must be on or off");
229
+ }
227
230
  const adapters = selectAdapters(options.adapter);
228
231
  const home = options.home ?? runtime.env?.HOME ?? homedir();
229
232
  const { data: dataHome } = platformPaths({ platform: runtime.platform,
@@ -261,6 +264,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
261
264
  ? await decideDelivery({ options, detected, recorded, runtime, dryRun })
262
265
  : { deliveryByAdapter: {}, asked: [], notes: [] };
263
266
  const plan = planInstallation({ adapters, detected, context, action, recorded,
267
+ indicator: options.indicator,
264
268
  accVersion, allowDowngrade: options.downgrade === true, requested,
265
269
  deliveryByAdapter: decided.deliveryByAdapter,
266
270
  deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
@@ -5,10 +5,6 @@
5
5
  import { AccError, CONFIG_FILENAME, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS,
6
6
  failure, ok }
7
7
  from "@agents-can-communicate/protocol";
8
- import { createCoordinationService } from "@agents-can-communicate/core";
9
- import { shellQuote } from "@agents-can-communicate/adapter-sdk";
10
- import { describeNativeReason } from "@agents-can-communicate/installer";
11
- import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
12
8
 
13
9
  import { parseArgs, positiveNumber } from "./args.mjs";
14
10
 
@@ -16,54 +12,13 @@ import { parseArgs, positiveNumber } from "./args.mjs";
16
12
  // with the argument already half-applied.
17
13
  const usage = message => new AccError(EXIT.USAGE, message);
18
14
  import { commandHelpText, describeCommand, describeCommands, helpText } from "./help.mjs";
19
- import { runUpdateCommand } from "./update-command.mjs";
20
- import { runConfigCommand } from "./config-command.mjs";
21
- import { ALL_ADAPTERS, runInstallCommand } from "./install-command.mjs";
22
- import { runDoctor } from "./doctor-command.mjs";
23
- import { runManagementDoctor } from "./managed-runtime/diagnostics.mjs";
24
- import { runStoreMigration } from "./store-migration-command.mjs";
15
+ import { loadAdapters } from "./adapter-catalog.mjs";
25
16
  import { createGitProbe } from "./git-probe.mjs";
26
- import { canonicalClaim } from "./claim-spelling.mjs";
27
17
  import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
28
18
  import { resolveOwner } from "./session-owner.mjs";
29
- import { discoverWorkspace } from "./workspace-discovery.mjs";
30
- import { resolveSavedHookWorkspace } from "./hook-workspace.mjs";
31
19
 
32
20
  const DEFAULT_CADENCE_MS = 30_000;
33
21
 
34
- /**
35
- * The claim a caller means when they name a resource instead of an id.
36
- *
37
- * `acc claim` takes `--resource`; `acc release` took `--claim`. The asymmetry
38
- * cost every caller - a person and an agent alike - a round trip through
39
- * `acc status --json` to find an id they never chose. The id is still accepted,
40
- * and is still the precise answer when two sessions hold the same resource and
41
- * an authority is releasing someone else's.
42
- */
43
- async function claimOn(options, context) {
44
- if (options.resource === undefined) {
45
- throw new AccError(EXIT.USAGE,
46
- "release needs the claim to give back: --resource is what you claimed, "
47
- + "--claim is its id");
48
- }
49
- const resource = await canonicalClaim(options.resource, context.descriptor);
50
- const { claims } = await context.service.collectStatus({});
51
- // An authority releasing another session's claim is not looking for its own.
52
- const mine = claims.filter(claim => claim.resource === resource
53
- && (options.authority !== undefined || claim.ownerSessionId === options.session));
54
-
55
- if (mine.length === 0) {
56
- throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
57
- { resource, held: claims.map(claim => claim.resource) });
58
- }
59
- if (mine.length > 1) {
60
- throw new AccError(EXIT.DATA,
61
- `${mine.length} claims on ${resource}; name the one you mean with --claim`,
62
- { resource, claims: mine.map(claim => claim.claimId) });
63
- }
64
- return mine[0].claimId;
65
- }
66
-
67
22
  /**
68
23
  * Where this workspace's state lives, without reading a byte of it.
69
24
  *
@@ -72,6 +27,10 @@ async function claimOn(options, context) {
72
27
  * looking is safe.
73
28
  */
74
29
  async function locateContext(options, runtime) {
30
+ // Workspace discovery reads through the store's no-follow opens, so it is
31
+ // loaded by the commands that use a workspace, never by `version` (#270).
32
+ const [{ discoverWorkspace }, { resolveSavedHookWorkspace }] = await Promise.all([
33
+ import("./workspace-discovery.mjs"), import("./hook-workspace.mjs")]);
75
34
  const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
76
35
  env: runtime.env });
77
36
  const discovery = {
@@ -92,6 +51,10 @@ async function locateContext(options, runtime) {
92
51
  }
93
52
 
94
53
  async function withService({ descriptor, paths, dataHome }, runtime) {
54
+ // Loaded here rather than at the top: `version`, `help` and the commands
55
+ // that work on a machine never open a store (#270).
56
+ const [{ openFilesystemStore }, { createCoordinationService }] = await Promise.all([
57
+ import("@agents-can-communicate/storage-filesystem"), import("@agents-can-communicate/core")]);
95
58
  const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
96
59
  ids: runtime.ids, workspaceId: descriptor.id });
97
60
  const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
@@ -153,35 +116,10 @@ function obligationFor(kind, explicit, addressed) {
153
116
  return obligation;
154
117
  }
155
118
 
156
- export function recordedText(message, delivery) {
157
- const diagnostics = delivery.map(item => item.outcome === "offered"
158
- ? `live offered to ${item.recipientParticipantId} via ${item.transport}`
159
- : item.outcome === "woken" && item.pendingApproval === true
160
- ? `sent a wake to ${item.recipientParticipantId} via ${item.transport}, which its session holds `
161
- + "for approval because it bypasses permission prompts; the message arrives with its next turn"
162
- : item.outcome === "woken"
163
- ? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
164
- : item.outcome === "queued"
165
- ? item.errorCode === "transport_permission_denied"
166
- ? "live offer blocked by sender permissions; message remains queued; retry the same command "
167
- + "through approved execution"
168
- + (typeof message.clientMessageId === "string"
169
- ? ` with --client-message-id ${shellQuote(message.clientMessageId)}` : " with its original retry key")
170
- : item.errorCode === "no_live_transport"
171
- ? `${item.recipientParticipantId} has no live transport`
172
- + `${item.nativeReasonCode ? ` (${describeNativeReason(item.nativeReasonCode,
173
- { launchOption: item.nativeLaunchOption })})` : ""}`
174
- + "; the message waits in its inbox"
175
- : item.errorCode === "recipient_offline" && item.endReason === "clear"
176
- ? `${item.recipientParticipantId}'s conversation was cleared (/clear); the message waits `
177
- + "until that conversation resumes"
178
- : item.errorCode === "recipient_offline"
179
- ? `${item.recipientParticipantId} has no open session; the message waits until it starts `
180
- + "or resumes one"
181
- : `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
182
- : `delivery already ${item.outcome}`);
183
- return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
184
- }
119
+ // What a sent message met, in words. Its diagnostics load with the first
120
+ // message a command sends, not with `acc version` (#270).
121
+ const describeRecorded = async (message, delivery) =>
122
+ (await import("./recorded-text.mjs")).recordedText(message, delivery);
185
123
 
186
124
  export async function recordAndOffer({ record, router, selectMessage = value => value }) {
187
125
  const recorded = await record();
@@ -212,7 +150,7 @@ export async function recordAndOffer({ record, router, selectMessage = value =>
212
150
  * A live binding wakes on the router's own rule: a current lease, or a lapsed
213
151
  * one the adapter can re-verify. Without that the next send is durable.
214
152
  */
215
- export function presenceBreakdown(participants = [], adapters = ALL_ADAPTERS()) {
153
+ export function presenceBreakdown(participants = [], adapters = []) {
216
154
  const nextTurn = new Set(adapters.filter(adapter => adapter.capabilities?.delivery?.nextTurn === true)
217
155
  .map(adapter => adapter.id));
218
156
  const refreshes = new Set(adapters.filter(adapter => typeof adapter.refreshNativeSession === "function")
@@ -245,13 +183,13 @@ export function describePresence({ live = 0, stale = 0 } = {},
245
183
  return `${live} live (${parts.join("; ")})`;
246
184
  }
247
185
 
248
- export function describeStatus(status) {
186
+ export function describeStatus(status, adapters = []) {
249
187
  // `live` counts everyone present, which includes sessions gone stale - a
250
188
  // client that exited without its session being closed keeps a record that
251
189
  // stops being answered but does not disappear. Printing the number alone
252
190
  // said "1 live" about a workspace where the last agent had left minutes
253
191
  // before, which is the one thing a person reads this line to find out.
254
- const line = `${describePresence(status.counts, presenceBreakdown(status.participants))}; `
192
+ const line = `${describePresence(status.counts, presenceBreakdown(status.participants, adapters))}; `
255
193
  + `${status.counts.claims} claim(s); `
256
194
  + `protection ${status.protection}`;
257
195
  // A transport accepting a message is not a model reading it. Said only when
@@ -327,25 +265,8 @@ const HANDLERS = Object.freeze({
327
265
  return { data: intent, text: `intent: ${intent.summary}` };
328
266
  },
329
267
 
330
- claim: async ({ options, context }) => {
331
- const claim = await context.service.acquireClaim({ sessionId: options.session,
332
- generation: options.generation,
333
- resource: await canonicalClaim(options.resource, context.descriptor),
334
- mode: options.mode ?? "exclusive", enforcement: options.enforcement ?? "advisory",
335
- reason: options.reason ?? "unspecified",
336
- leaseSeconds: options.lease ? positiveNumber(options.lease, "lease") : undefined,
337
- descriptor: context.descriptor });
338
- return { data: claim, text: `claimed ${claim.resource}` };
339
- },
340
-
341
- release: async ({ options, context }) => {
342
- const claimId = options.claim ?? await claimOn(options, context);
343
- const request = { claimId, sessionId: options.session,
344
- generation: options.generation, authority: options.authority, reason: options.reason };
345
- if (options.authority === undefined) await context.service.releaseClaim(request);
346
- else await context.service.forceReleaseClaim(request);
347
- return { data: { claimId }, text: `released ${claimId}` };
348
- },
268
+ claim: async input => (await import("./claim-command.mjs")).runClaimCommand(input),
269
+ release: async input => (await import("./claim-command.mjs")).runReleaseCommand(input),
349
270
 
350
271
  message: async ({ options, context }) => {
351
272
  const kind = options.type ?? "note";
@@ -360,7 +281,7 @@ const HANDLERS = Object.freeze({
360
281
  subject: options.subject, body: options.body, descriptor: context.descriptor }) });
361
282
  const message = routed.recorded;
362
283
  return { data: { message, delivery: routed.delivery },
363
- text: recordedText(message, routed.delivery) };
284
+ text: await describeRecorded(message, routed.delivery) };
364
285
  },
365
286
 
366
287
  inbox: async ({ options, context }) => {
@@ -387,7 +308,7 @@ const HANDLERS = Object.freeze({
387
308
  subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
388
309
  const result = routed.recorded;
389
310
  return { data: { message: result.reply, receipt: result.receipt, delivery: routed.delivery },
390
- text: `${recordedText(result.reply, routed.delivery)}; acknowledged ${result.receipt.messageId}` };
311
+ text: `${await describeRecorded(result.reply, routed.delivery)}; acknowledged ${result.receipt.messageId}` };
391
312
  },
392
313
 
393
314
  request: async ({ options, context }) => {
@@ -405,7 +326,7 @@ const HANDLERS = Object.freeze({
405
326
  }) });
406
327
  const message = routed.recorded;
407
328
  return { data: { message, delivery: routed.delivery },
408
- text: recordedText(message, routed.delivery) };
329
+ text: await describeRecorded(message, routed.delivery) };
409
330
  },
410
331
 
411
332
  ack: async ({ options, context }) => {
@@ -424,17 +345,20 @@ const HANDLERS = Object.freeze({
424
345
  remaining: options.remaining ?? [], blockers: options.blocker ?? [] }) });
425
346
  const handoff = routed.recorded;
426
347
  return { data: { message: handoff.message, delivery: routed.delivery },
427
- text: recordedText(handoff.message, routed.delivery) };
348
+ text: await describeRecorded(handoff.message, routed.delivery) };
428
349
  },
429
350
 
430
351
  status: async ({ options, context, runtime }) => {
431
352
  await registerNativeSessions(runtime, context);
432
353
  const status = await context.service.collectStatus({
433
354
  participantId: options.participant, all: options.all === true });
434
- return { data: status, text: describeStatus(status) };
355
+ // Only the clients present here say how their idle sessions are reached.
356
+ const adapters = await loadAdapters((status.participants ?? []).map(item => item.harness));
357
+ return { data: status, text: describeStatus(status, Object.values(adapters)) };
435
358
  },
436
359
 
437
360
  config: async ({ options, runtime }) => {
361
+ const { runConfigCommand } = await import("./config-command.mjs");
438
362
  const result = await runConfigCommand({ subcommand: options.subcommand,
439
363
  cwd: options.cwd ?? runtime.cwd,
440
364
  // A pipe is not a person. Without a TTY there is nobody to answer, so the
@@ -461,11 +385,11 @@ const HANDLERS = Object.freeze({
461
385
  text: result.written ? `wrote ${result.file}` : `not written: ${result.file}` };
462
386
  },
463
387
 
464
- install: async ({ options, runtime }) =>
465
- runInstallCommand({ options, runtime, action: "install" }),
388
+ install: async ({ options, runtime }) => (await import("./install-command.mjs"))
389
+ .runInstallCommand({ options, runtime, action: "install" }),
466
390
 
467
- uninstall: async ({ options, runtime }) =>
468
- runInstallCommand({ options, runtime, action: "uninstall" }),
391
+ uninstall: async ({ options, runtime }) => (await import("./install-command.mjs"))
392
+ .runInstallCommand({ options, runtime, action: "uninstall" }),
469
393
 
470
394
  prune: async ({ options, context }) => {
471
395
  const result = await context.service.prune({
@@ -485,9 +409,12 @@ const HANDLERS = Object.freeze({
485
409
  return { data: result, text };
486
410
  },
487
411
 
488
- doctor: async input => input.options.migrateStore ? runStoreMigration(input) : runDoctor(input),
412
+ doctor: async input => input.options.migrateStore
413
+ ? (await import("./store-migration-command.mjs")).runStoreMigration(input)
414
+ : (await import("./doctor-command.mjs")).runDoctor(input),
489
415
 
490
- update: async ({ options, runtime }) => runUpdateCommand({ options, runtime }),
416
+ update: async ({ options, runtime }) => (await import("./update-command.mjs"))
417
+ .runUpdateCommand({ options, runtime }),
491
418
 
492
419
  help: async ({ options }) => {
493
420
  if (options.helpCommand === undefined) {
@@ -549,8 +476,9 @@ export async function main(argv, runtime) {
549
476
  const options = context === null ? parsed.options
550
477
  : await resolveOwner({ command: parsed.command, options: parsed.options,
551
478
  context, env: runtime.env });
552
- const { data, text, error: outcome } = await (managementDoctor ? runManagementDoctor : HANDLERS[parsed.command])(
553
- { options, context, runtime });
479
+ const handler = managementDoctor
480
+ ? (await import("./managed-runtime/diagnostics.mjs")).runManagementDoctor : HANDLERS[parsed.command];
481
+ const { data, text, error: outcome } = await handler({ options, context, runtime });
554
482
  // A handler may have done real work and still failed: `acc install` writes
555
483
  // for the clients it could and reports the one it could not. The data is
556
484
  // printed either way, and the command still fails.
@@ -32,6 +32,25 @@ export function invokedDirectly(url) {
32
32
  catch { return false; }
33
33
  }
34
34
 
35
+ /** The display is an observation, not an admitted runtime user. A stable
36
+ * launcher resolves the active generation on every refresh, so an old plugin
37
+ * never retains a path that a same-contract update may reclaim. A concurrent
38
+ * replacement fails open and the next display refresh tries the new pointer. */
39
+ export async function runIndicatorEntry({ managerRoot }) {
40
+ try {
41
+ const root = await canonicalManagerRoot(managerRoot);
42
+ const control = await readControl(root);
43
+ if (!control) throw new Error("indicator runtime missing");
44
+ await import(pathToFileURL(path.join(control.active.root, "bin", "acc-indicator.mjs")).href);
45
+ } catch {
46
+ const report = { health: "problem", reception: null, reasonCode: "indicator_unavailable",
47
+ label: "ACC !", detail: "ACC's installed indicator is unavailable.",
48
+ action: "Run acc update to finish runtime activation, or acc install --indicator on to repair it." };
49
+ process.stdout.write(process.argv.includes("--json") ? JSON.stringify(report) + "\n"
50
+ : "ACC ! · acc doctor\n");
51
+ }
52
+ }
53
+
35
54
  function unavailable(kind) {
36
55
  // Never reflect control file content or payloads into a client diagnostic.
37
56
  if (kind === "acc-hook" || kind === "acc-antigravity-relay") {
@@ -16,6 +16,7 @@ async function durableFile(file, bytes, mode = 0o600) {
16
16
  export const stablePaths = root => ({
17
17
  cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
18
18
  antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
19
+ indicatorRunner: path.join(root, "bin", "acc-indicator.mjs"),
19
20
  });
20
21
 
21
22
  /** Publish immutable launcher modules, then atomically replace each tiny entry.
@@ -47,9 +48,10 @@ export async function writeLaunchers(root, packageRoot) {
47
48
  } finally { await removeTree(staging); }
48
49
  const bin = path.join(root, "bin");
49
50
  await managedDirectory(bin, { create: true });
50
- for (const kind of ENTRY_KINDS) {
51
- const bytes = `#!/usr/bin/env node\nimport { runEntry } from "../launchers/${id}/entry.mjs";\n`
52
- + `await runEntry(${JSON.stringify({ kind, packageRoot, managerRoot: root, managedRequired: true })});\n`;
51
+ for (const kind of [...ENTRY_KINDS, "acc-indicator"]) {
52
+ const entry = kind === "acc-indicator" ? "runIndicatorEntry" : "runEntry";
53
+ const bytes = `#!/usr/bin/env node\nimport { ${entry} } from "../launchers/${id}/entry.mjs";\n`
54
+ + `await ${entry}(${JSON.stringify({ kind, packageRoot, managerRoot: root, managedRequired: true })});\n`;
53
55
  const temporary = path.join(bin, `.${randomUUID()}.tmp`);
54
56
  try {
55
57
  await durableFile(temporary, bytes, 0o700);
@@ -99,7 +99,7 @@ export async function updateNativeSessions(adapters, { service, status, root, no
99
99
  const failures = {
100
100
  transport_permission_denied: "this command cannot access the local delivery channel; "
101
101
  + "check the sender's command permissions",
102
- client_process_unknown: "the client process could not be identified; start a new client session",
102
+ client_process_unknown: "the client process could not be identified; the next prompt looks for it again",
103
103
  handshake_failed: "the session handshake failed; check the client's integration/channel setup",
104
104
  handshake_timeout: "the session handshake timed out; check the local channel and retry on the next turn",
105
105
  // Since the contract gate shipped, this reason means the version now serving
@@ -0,0 +1,32 @@
1
+ import { shellQuote } from "@agents-can-communicate/adapter-sdk";
2
+ import { describeNativeReason } from "@agents-can-communicate/installer";
3
+
4
+ export function recordedText(message, delivery) {
5
+ const diagnostics = delivery.map(item => item.outcome === "offered"
6
+ ? `live offered to ${item.recipientParticipantId} via ${item.transport}`
7
+ : item.outcome === "woken" && item.pendingApproval === true
8
+ ? `sent a wake to ${item.recipientParticipantId} via ${item.transport}, which its session holds `
9
+ + "for approval because it bypasses permission prompts; the message arrives with its next turn"
10
+ : item.outcome === "woken"
11
+ ? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
12
+ : item.outcome === "queued"
13
+ ? item.errorCode === "transport_permission_denied"
14
+ ? "live offer blocked by sender permissions; message remains queued; retry the same command "
15
+ + "through approved execution"
16
+ + (typeof message.clientMessageId === "string"
17
+ ? ` with --client-message-id ${shellQuote(message.clientMessageId)}` : " with its original retry key")
18
+ : item.errorCode === "no_live_transport"
19
+ ? `${item.recipientParticipantId} has no live transport`
20
+ + `${item.nativeReasonCode ? ` (${describeNativeReason(item.nativeReasonCode,
21
+ { launchOption: item.nativeLaunchOption })})` : ""}`
22
+ + "; the message waits in its inbox"
23
+ : item.errorCode === "recipient_offline" && item.endReason === "clear"
24
+ ? `${item.recipientParticipantId}'s conversation was cleared (/clear); the message waits `
25
+ + "until that conversation resumes"
26
+ : item.errorCode === "recipient_offline"
27
+ ? `${item.recipientParticipantId} has no open session; the message waits until it starts `
28
+ + "or resumes one"
29
+ : `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
30
+ : `delivery already ${item.outcome}`);
31
+ return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
32
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.10.4",
3
+ "version": "0.11.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,62 @@
1
+ import { AccError, EXIT, assertMatchableResource, assertPortableId,
2
+ assertResourceUri, normaliseResource } from "@agents-can-communicate/protocol";
3
+
4
+ const usage = message => { throw new AccError(EXIT.USAGE, message); };
5
+
6
+ export function uniqueList(value, name, normalise = item => item) {
7
+ if (!Array.isArray(value) || value.length === 0) usage(`${name} must be a nonempty array`);
8
+ const result = value.map(item => {
9
+ if (typeof item !== "string" || item.length === 0) usage(`${name} must contain nonempty strings`);
10
+ return normalise(item);
11
+ });
12
+ if (new Set(result).size !== result.length) usage(`${name} contains duplicate selectors`);
13
+ return result;
14
+ }
15
+
16
+ export function claimResources(value) {
17
+ return uniqueList(value, "resources", resource => {
18
+ assertMatchableResource(resource, usage);
19
+ const normalised = normaliseResource(resource);
20
+ try { return assertResourceUri(normalised, "resource"); }
21
+ catch (error) {
22
+ if (!(error instanceof AccError)) throw error;
23
+ throw new AccError(EXIT.USAGE, error.message, error.details);
24
+ }
25
+ });
26
+ }
27
+
28
+ export function releaseSelectors(input) {
29
+ const present = ["claimIds", "resources", "all"].filter(key => input[key] !== undefined);
30
+ if (present.length !== 1 || (present[0] === "all" && input.all !== true)) {
31
+ usage("release needs exactly one of claimIds, resources, or all: true");
32
+ }
33
+ if (input.authority !== undefined) usage("batch release does not accept force-release authority");
34
+ if (input.all === true) return { all: true };
35
+ if (input.resources !== undefined) return { resources: claimResources(input.resources) };
36
+ return { claimIds: uniqueList(input.claimIds, "claimIds", id => {
37
+ try { return assertPortableId(id, "claim id"); }
38
+ catch (error) {
39
+ if (!(error instanceof AccError)) throw error;
40
+ throw new AccError(EXIT.USAGE, error.message, error.details);
41
+ }
42
+ }) };
43
+ }
44
+
45
+ export function selectReleaseClaims(tx, selectors, sessionId) {
46
+ if (selectors.all === true) return tx.list("claim", claim => claim.ownerSessionId === sessionId);
47
+ if (selectors.claimIds !== undefined) return selectors.claimIds.map(claimId => {
48
+ const claim = tx.get("claim", claimId);
49
+ if (claim === null) throw new AccError(EXIT.CONFLICT, "the claim does not exist", { claimId });
50
+ return claim;
51
+ });
52
+ const claims = tx.list("claim");
53
+ return selectors.resources.map(resource => {
54
+ const mine = claims.filter(claim => claim.resource === resource && claim.ownerSessionId === sessionId);
55
+ if (mine.length === 0) throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
56
+ { resource, held: claims.map(claim => claim.resource) });
57
+ if (mine.length > 1) throw new AccError(EXIT.DATA,
58
+ `${mine.length} claims on ${resource}; name the one you mean with --claim`,
59
+ { resource, claims: mine.map(claim => claim.claimId) });
60
+ return mine[0];
61
+ });
62
+ }