agents-can-communicate 0.6.0 → 0.6.2

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 (34) hide show
  1. package/docs/ADAPTER_AUTHORING.md +19 -16
  2. package/docs/ARCHITECTURE.md +3 -3
  3. package/docs/CAPABILITIES.md +17 -15
  4. package/docs/CONCEPTS.md +2 -2
  5. package/docs/GETTING_STARTED.md +5 -5
  6. package/docs/GLOSSARY.md +1 -1
  7. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  8. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  9. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +4 -5
  10. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  11. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +2 -2
  12. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  13. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  14. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  15. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  16. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  17. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  18. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +6 -32
  19. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +73 -22
  20. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
  21. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  23. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +1 -2
  24. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  25. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  26. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +12 -6
  27. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  28. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +16 -4
  29. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  30. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  31. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  32. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +23 -13
  33. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +18 -1
  34. package/package.json +1 -1
@@ -118,22 +118,25 @@ example is not a capture. Failed experiments stay in the manifest as `fail`; the
118
118
  the false value and can never enable it.
119
119
 
120
120
  `effectiveCapabilities(adapter, { clientVersion, platform })` returns the full boolean
121
- shape for the installed client. Only an exact passing version/platform match remains true.
122
- Unreadable, unknown, or mismatched clients degrade every uncertified row to false.
123
-
124
- An adapter for a client that ships often may declare a floor per platform:
125
-
126
- ```js
127
- certificationFloor: { "darwin-arm64": "1.2.7" },
128
- ```
129
-
130
- A stable version at or above the floor, on that platform, is then judged by the floor
131
- version's evidence for any capability it has no capture of its own for. A later capture
132
- wins for its own version, capability by capability - a recorded failure turns that
133
- capability off for that version and leaves the rest on the floor. Earlier versions,
134
- prereleases and other platforms stay uncertified, and `defineAdapter` refuses a floor
135
- that names a version with no passing evidence on its platform. Antigravity CLI declares
136
- one; every other adapter certifies exact versions only.
121
+ shape for the installed client, resolving each capability independently from the evidence
122
+ that client can reach:
123
+
124
+ 1. Take the rows for that capability, and keep those whose version is at or below the
125
+ client's. A prerelease is ordered by its release triple, so `1.3.0-rc.1` is judged as
126
+ `1.3.0`, and a version that cannot be read reaches every row.
127
+ 2. Among those, prefer the rows that name this platform; with none, use them all. A loss
128
+ recorded for one platform at 1.3.0 says nothing about that platform at 1.2.3, where
129
+ another platform's passing capture is the only evidence in reach.
130
+ 3. The highest version left decides. The capability is on when every row at that version
131
+ passes, so a recorded loss wins a tie between platforms.
132
+
133
+ A client older than every row gets nothing, and so does a capability whose deciding row
134
+ records a failure. `capabilityEvidence(adapter, facts, capability)` returns that verdict
135
+ with its reason - `undeclared`, `unobserved`, `older-than-evidence` or `recorded-failure` -
136
+ and the version that decided, so a refusal can name the evidence rather than the client.
137
+
138
+ Recording a regression is an ordinary capture with `result: "fail"` at the version where
139
+ the loss was observed; it applies forward until a later row passes again.
137
140
 
138
141
  The backing methods for delivery are `renderContextResult()` for `nextTurn`,
139
142
  `offerMessage()` for `livePush`, and `routeReply()` for `replyRoute`.
@@ -87,12 +87,12 @@ if registration fails. This does not create a session or revive a closed one.
87
87
 
88
88
  ## Certified capability versus current reachability
89
89
 
90
- Ordinary hook capabilities say an exact client version on an exact platform passed a
91
- captured behavior. Native live delivery uses a separate contract: installation checks the
90
+ Ordinary hook capabilities say a captured behaviour passed, and apply forward from the
91
+ version that recorded it until a later capture changes it. Native live delivery uses a separate contract: installation checks the
92
92
  captured minimum, platform, and current feature probe, then each session publishes a binding
93
93
  only after a generation-bound handshake. The router trusts that admission and verifies the
94
94
  binding, recipient policy, declared adapter capability, and adapter response; it does not add
95
- a third exact-version certification check. A binding says what that current generation
95
+ a third certification check. A binding says what that current generation
96
96
  exposes and whether its lease is current, while recipient policy says whether it may spend a
97
97
  turn.
98
98
 
@@ -3,22 +3,25 @@
3
3
  Use this page to set expectations after installation. Integration means ACC can introduce
4
4
  peer awareness and coordination instructions; it does not guarantee what a model will do
5
5
  with them. Delivery also varies independently from awareness. The durable inbox works for
6
- every participant, supported exact versions may add next-turn delivery, and the
6
+ every participant, a client at or after a captured version may add next-turn delivery, and the
7
7
  experimental Codex LocalDaemon and Claude Code Channel paths can deliver while a session is idle.
8
8
 
9
9
  Capability honesty separates four questions that are easy to collapse:
10
10
 
11
- 1. **Certified support** — did this exact client version and platform pass a shipped
12
- real-client fixture?
11
+ 1. **Certified support** — does the evidence this client can reach show the capability
12
+ passing in a shipped real-client fixture?
13
13
  2. **Current reachability** — does one current session generation expose a live binding
14
14
  whose lease is valid now?
15
15
  3. **Recipient policy** — did that recipient opt into spending a turn for this message
16
16
  kind?
17
17
  4. **Fallback** — what durable path remains when any earlier answer is no?
18
18
 
19
- A source method or vendor documentation is not certification. Uncaptured hook versions and
20
- unsupported platforms degrade to false, except that an adapter may declare a captured version
21
- as a floor for later stable releases on the same platform - Antigravity CLI does. Native minimum-based eligibility is separate. No weaker session inherits a stronger peer's capability.
19
+ A source method or vendor documentation is not certification. A capture applies forward: from
20
+ the version that recorded it until a later capture changes that capability, and to every
21
+ platform until one of them records something of its own. A client older than every capture
22
+ degrades to false, and so does a capability a capture recorded as failing. Native
23
+ minimum-based eligibility is separate, and stays per-platform. No weaker session inherits a
24
+ stronger peer's capability.
22
25
 
23
26
  Run `acc doctor` in the project when observed behavior differs from this page. It reports
24
27
  the installed client version, platform, effective capability, and fallback instead of
@@ -26,10 +29,9 @@ assuming that a newer or differently packaged client behaves like a captured one
26
29
 
27
30
  ## Certified support
28
31
 
29
- Passing evidence currently ships for these exact versions on `darwin-arm64`. Antigravity
30
- CLI's column also covers every later stable release there: its adapter declares 1.2.7 as a
31
- certification floor, so a newer version is judged by the 1.2.7 captures until one of its
32
- own says otherwise.
32
+ Each column names the version that recorded the capture, on `darwin-arm64`. Every later
33
+ version of that client reads it, on every platform, until a capture of its own says
34
+ otherwise. A client older than the version named here is unproven and gets nothing.
33
35
 
34
36
  | Capability | Antigravity 1.2.7 | Codex 0.147.0 | Claude Code 2.1.233 | Gemini CLI 0.57.0 | Grok 1.0.13 | Kimi 0.36.1 |
35
37
  |---|---:|---:|---:|---:|---:|---:|
@@ -46,7 +48,7 @@ own says otherwise.
46
48
  Every other capability in the closed shape defaults to false, including session resume,
47
49
  child sessions, startup or safe-point injection, and before-read guards.
48
50
 
49
- The native rows remain `no` for the older exact hook versions in this matrix.
51
+ The native rows are `no` at the hook versions this matrix names.
50
52
  Separate installed-client captures establish Codex `livePush` on 0.152.1 and
51
53
  0.153.4, Claude Code `livePush` plus `replyRoute` on 2.1.258 and 2.1.260, and
52
54
  Antigravity CLI `livePush` on 1.2.7 and later through a relay the agent starts in its own shell.
@@ -58,12 +60,12 @@ The limitations belong next to the adapters they affect:
58
60
 
59
61
  | Adapter | Exact limitation and evidence |
60
62
  |---|---|
61
- | Antigravity CLI | 1.2.7 on darwin-arm64, captured in print mode, and later stable releases by certification floor. Only `SessionStart`, `PreInvocation`, `PostInvocation` and `Stop` load; `SessionEnd`, `PreToolUse` and `PostToolUse` are accepted into the config file and silently dropped, so there is no tool guard and no session-end deregistration - a session goes offline by presence age or an explicit `acc finish`. Payloads carry no `hook_event_name`, so each registered command passes its own event name. The end-of-turn `Stop` continuation reaches the model and is a bounded nudge, not a gate: ACC continues a turn at most once and fails open, and the client caps consecutive continuations itself (vendor 1.1.9). `agy agentapi send-message` can wake an idle session - captured - but only with that session's language-server address and CSRF token, which exist in the agent's own shell and in no hook. ACC does not take that token, so live push and reply routing are false. A peer message that arrives while the model writes its last answer is carried by the `Stop` continuation instead. A write that parses can register nothing, so install and doctor read `agy -p "/hooks"` back instead of trusting the file. Live push (1.2.7, darwin-arm64, TUI only, experimental, recorded opt-in) runs through a relay the agent starts once per conversation from its own shell - the only process holding the session endpoint - after ACC's context asks it to; the operator approves that command at the client's permission prompt. An idle session wakes; a busy one sees the message after its running answer, or at the next model invocation when the turn waits on a tool. Print mode and the first session in a folder trusted at that launch get no relay. |
62
- | Codex | Exact 0.147.0 next-turn context requires plugin trust. The observed stock 0.153.4 upgrade from ACC 0.3.1 to 0.4 required fresh review of five modified hook definitions; a subsequent restart retained all five active (activation evidence, not new event certification). LocalDaemon native delivery was captured through the installed package on 0.152.1 and 0.153.4, darwin-arm64; minimum 0.152.1, recorded opt-in, current feature probe and exact thread/cwd/process/version/protocol checks are required. Ordinary launch preserves the receiver workspace without ACC arguments or daemon ownership. Embedded or unreachable sessions keep their inbox. Native `replyRoute` remains false. |
63
+ | Antigravity CLI | 1.2.7 on darwin-arm64, captured in print mode, and every later release by the forward rule. Only `SessionStart`, `PreInvocation`, `PostInvocation` and `Stop` load; `SessionEnd`, `PreToolUse` and `PostToolUse` are accepted into the config file and silently dropped, so there is no tool guard and no session-end deregistration - a session goes offline by presence age or an explicit `acc finish`. Payloads carry no `hook_event_name`, so each registered command passes its own event name. The end-of-turn `Stop` continuation reaches the model and is a bounded nudge, not a gate: ACC continues a turn at most once and fails open, and the client caps consecutive continuations itself (vendor 1.1.9). `agy agentapi send-message` can wake an idle session - captured - but only with that session's language-server address and CSRF token, which exist in the agent's own shell and in no hook. ACC does not take that token, so live push and reply routing are false. A peer message that arrives while the model writes its last answer is carried by the `Stop` continuation instead. A write that parses can register nothing, so install and doctor read `agy -p "/hooks"` back instead of trusting the file. Live push (1.2.7, darwin-arm64, TUI only, experimental, recorded opt-in) runs through a relay the agent starts once per conversation from its own shell - the only process holding the session endpoint - after ACC's context asks it to; the operator approves that command at the client's permission prompt. An idle session wakes; a busy one sees the message after its running answer, or at the next model invocation when the turn waits on a tool. Print mode and the first session in a folder trusted at that launch get no relay. |
64
+ | Codex | Next-turn context, captured on 0.147.0, requires plugin trust. The observed stock 0.153.4 upgrade from ACC 0.3.1 to 0.4 required fresh review of five modified hook definitions; a subsequent restart retained all five active (activation evidence, not new event certification). LocalDaemon native delivery was captured through the installed package on 0.152.1 and 0.153.4, darwin-arm64; minimum 0.152.1, recorded opt-in, current feature probe and exact thread/cwd/process/version/protocol checks are required. Ordinary launch preserves the receiver workspace without ACC arguments or daemon ownership. Embedded or unreachable sessions keep their inbox. Native `replyRoute` remains false. |
63
65
  | Claude Code | 2.1.233 next-turn delivery waits for the next user prompt. A 2.1.258 Channel capture proved idle offer, busy queue-after-turn, explicit reply, duplicate suppression, and durable fallback, so `delivery.livePush` and `delivery.replyRoute` are live capabilities behind the native contract (experimental, off until opted in; Claude's development-channel warning is vendor-owned and visible). |
64
- | Gemini CLI | Only 0.57.0 has package-shipped next-turn certification. Its TUI has no captured external wake or queue interface and `--acp` changes launch ownership, so native delivery is fallback-only; live push and reply routing remain false. |
66
+ | Gemini CLI | Package-shipped next-turn certification starts at 0.57.0. Its TUI has no captured external wake or queue interface and `--acp` changes launch ownership, so native delivery is fallback-only; live push and reply routing remain false. |
65
67
  | Grok | The Grok 1.0.24 installed-client check observed own CLI arguments after a terminal result, followed by owned work, message, and finish calls. This identity-only path does not certify peer-context injection. Documentation-shaped payloads do not count as real captures. The public leader surface exposed no proven addressed injection into an ordinary TUI session, so native delivery is `awaiting_compatibility_capture`; all capabilities remain false. |
66
- | Kimi Code | 0.36.1 has next-turn and guard evidence, plus a 60-second heartbeat. Its server/queue APIs do not prove a transparent binding to an independently opened session, so native delivery is fallback-only. |
68
+ | Kimi Code | Next-turn and guard evidence was captured on 0.36.1, plus a 60-second heartbeat. Its server/queue APIs do not prove a transparent binding to an independently opened session, so native delivery is fallback-only. |
67
69
  | Generic MCP | As a receiver, tool polling is not next-turn injection, live push, or a native reply route. It has no write guard or client-lifecycle evidence. Outgoing messages may use an eligible recipient's opted-in native adapter. |
68
70
 
69
71
  `certification.json` beside each adapter is machine-readable. `COMPATIBILITY.md` records the
package/docs/CONCEPTS.md CHANGED
@@ -123,8 +123,8 @@ recoverable rather than creating a terminal failure state.
123
123
 
124
124
  ## Durable first, faster delivery second
125
125
 
126
- The durable inbox is universal. Exact-version certified adapters may also offer
127
- messages at the next normal turn. Codex LocalDaemon and Claude Code Channel add
126
+ The durable inbox is universal. An adapter whose captures reach the installed client may
127
+ also offer messages at the next normal turn. Codex LocalDaemon and Claude Code Channel add
128
128
  experimental live delivery, off by default and subject to recipient policy and
129
129
  current reachability. Codex can retain an eligible daemon thread after its terminal
130
130
  exits. [Capabilities](CAPABILITIES.md) gives the current limits.
@@ -53,7 +53,7 @@ are active; `acc doctor` leaves current readiness unverified and directs you to
53
53
  Read the delivery summary for each client. Live delivery needs an explicit opt-in and a
54
54
  verified channel in the current session. Codex can save your choice even when its local
55
55
  service is not running yet. Declining uses the reported fallback: `acc inbox`, or next-turn
56
- hooks only where the exact client version and platform are certified.
56
+ hooks wherever the client is at or after a captured version.
57
57
 
58
58
  An interactive install asks one default-No question for all selected clients that need a
59
59
  choice. Use `--delivery actionable|all` for explicit noninteractive consent. Use
@@ -131,10 +131,10 @@ or follow the interaction through [How ACC works](HOW_IT_WORKS.md).
131
131
  Every message is recorded before ACC attempts faster delivery. Every participant has a
132
132
  durable inbox, which is the universal recovery path.
133
133
 
134
- On exact client versions and platforms with captured support, Codex, Claude Code, Gemini
135
- CLI, Kimi Code and Antigravity CLI can receive a message at the next normal turn. That does
136
- not wake an idle session. Grok, generic MCP clients, unknown versions, and unsupported
137
- platforms use the durable inbox instead.
134
+ From the version each capture records onward, Codex, Claude Code, Gemini CLI, Kimi Code
135
+ and Antigravity CLI can receive a message at the next normal turn, on any platform. That
136
+ does not wake an idle session. Grok, generic MCP clients and clients older than every
137
+ capture use the durable inbox instead.
138
138
 
139
139
  Antigravity CLI carries one extra condition: its hooks are given a project directory only
140
140
  when the session has an open workspace. A session started without one attaches nothing, and
package/docs/GLOSSARY.md CHANGED
@@ -19,7 +19,7 @@
19
19
  - **Live push** — optional delivery through Claude Code Channel or Codex LocalDaemon to an already-running session, behind recipient opt-in. Both wait for an active turn to finish; [Capabilities](CAPABILITIES.md) gives the measured eligibility and limits.
20
20
  - **Recipient policy** — `off`, `actionable`, or `all`; opt-in permission to spend a turn, not a capability.
21
21
  - **Delivery binding** — ephemeral, generation-bound reachability data owned by an adapter.
22
- - **Fallback** — durable inbox or exact-certified next-turn recovery when live delivery is unavailable.
22
+ - **Fallback** — durable inbox or certified next-turn recovery when live delivery is unavailable.
23
23
  - **Managed / manual lifecycle** — whether hooks report ACC presence automatically; never ownership of the external client process.
24
24
  - **MCP participation** — manual incoming polling without native lifecycle, context, guards, or receive wake; outgoing messages can use an eligible recipient's opted-in native route.
25
25
  - **Bounded discovery** — inbox/history summary pages with limits and cursors; bodies require selection.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-antigravity",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "acc",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Coordinate this Antigravity CLI session with other AI agent sessions working in the same workspace."
5
5
  }
@@ -9,10 +9,10 @@ import { bindNativeSession, nativeActivationHint, offerMessage, planNativeActiva
9
9
  probeNativeDelivery, refreshNativeSession } from "./native-delivery.mjs";
10
10
  import { PROTOCOL_CONTRACT } from "./relay-endpoint.mjs";
11
11
 
12
- // The version this client has been captured on, and the floor of what is
13
- // certified: nothing earlier was measured, and later releases - this client
14
- // ships every few days - are judged by this capture until one of their own
15
- // says otherwise.
12
+ // The version this client has been captured on. Nothing earlier was measured,
13
+ // and later releases - this client ships every few days - are judged by this
14
+ // capture until one of their own says otherwise, which is how certification
15
+ // evidence reads everywhere since 0.6.2.
16
16
  export const ANTIGRAVITY_CLI_VERSION = "1.2.7";
17
17
 
18
18
  /**
@@ -65,7 +65,6 @@ export function createAntigravityAdapter() {
65
65
  client: { command: "agy", certificationName: "antigravity-cli",
66
66
  versionArgs: ["--version"] },
67
67
  certification,
68
- certificationFloor: { "darwin-arm64": ANTIGRAVITY_CLI_VERSION },
69
68
  capabilities: {
70
69
  lifecycle: { sessionStart: true },
71
70
  context: { beforeTurnInjection: true },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -46,8 +46,8 @@ export function createClaudeCodeAdapter() {
46
46
  guards: { beforeWrite: true, beforeShell: true },
47
47
  // nextTurn is the certified 2.1.233 hook projection; livePush and
48
48
  // replyRoute rest on the 2.1.258 Channel capture and the native contract
49
- // below. effectiveCapabilities() still gates every row on an exact
50
- // certified version; the native contract is the separate live rule.
49
+ // below. effectiveCapabilities() carries each of those captures forward
50
+ // to later clients; the native contract is the separate live rule.
51
51
  delivery: { nextTurn: true, livePush: true, replyRoute: true },
52
52
  },
53
53
  // The first passing capture is the shipped minimum; the research lower
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -3,7 +3,6 @@ import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protoc
3
3
  import { CAPABILITY_SHAPE, freezeCapabilities, validateCertification }
4
4
  from "./certification.mjs";
5
5
  import { validateNativeDeliveryContract } from "./native-delivery.mjs";
6
- import { parseStableVersion } from "./native-vocabulary.mjs";
7
6
 
8
7
  // The capability surface, documented in docs/ADAPTER_AUTHORING.md and measured
9
8
  // per client in docs/CAPABILITIES.md. False is the default for every
@@ -116,10 +115,13 @@ export function defineAdapter(manifest) {
116
115
  { evidenceClient: item.client, client });
117
116
  }
118
117
  }
119
- const floor = {};
118
+ // A floor said "judge later versions by this capture". Evidence now does that
119
+ // by itself, on every platform, so the field has nothing left to express and
120
+ // an adapter still declaring one is stale rather than strict. See
121
+ // docs/design/2026-09-22-certification-applies-forward.md.
120
122
  if (manifest.certificationFloor !== undefined) {
121
- floor.certificationFloor = validateCertificationFloor(manifest.certificationFloor,
122
- { certification, client: manifest.client.certificationName ?? manifest.client.command });
123
+ usage("certificationFloor was removed: certification evidence applies forward from the "
124
+ + "version that recorded it", { id: manifest.id });
123
125
  }
124
126
  const native = {};
125
127
  for (const method of ["refreshNativeSession", "retireNativeSession", "nativeActivationHint"]) {
@@ -140,37 +142,9 @@ export function defineAdapter(manifest) {
140
142
  return Object.freeze({
141
143
  ...manifest,
142
144
  certification,
143
- ...floor,
144
145
  ...native,
145
146
  capabilities: assertCapabilities(manifest.capabilities, manifest, certification),
146
147
  });
147
148
  }
148
149
 
149
- const FLOOR_PLATFORM = /^(?:darwin|linux|win32)-(?:arm64|x64)$/;
150
150
 
151
- /**
152
- * An optional per-platform floor: later stable versions are judged by this
153
- * version's evidence until a capture of their own says otherwise. It can only
154
- * name a version that has passing evidence on that platform, so a floor never
155
- * certifies anything nobody captured.
156
- */
157
- function validateCertificationFloor(value, { certification, client }) {
158
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
159
- usage("certificationFloor must map a platform to a stable version");
160
- }
161
- for (const [platform, version] of Object.entries(value)) {
162
- if (!FLOOR_PLATFORM.test(platform)) {
163
- usage(`certificationFloor names an unknown platform ${platform}`, { platform });
164
- }
165
- if (parseStableVersion(version) === null) {
166
- usage(`certificationFloor ${platform} must be a stable version`, { platform, version });
167
- }
168
- const proven = certification.evidence.some(item => item.result === "pass"
169
- && item.client === client && item.version === version && item.platform === platform);
170
- if (!proven) {
171
- usage(`certificationFloor ${version} on ${platform} has no passing evidence`,
172
- { platform, version });
173
- }
174
- }
175
- return Object.freeze({ ...value });
176
- }
@@ -2,8 +2,6 @@ import path from "node:path";
2
2
 
3
3
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
4
4
 
5
- import { compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
6
-
7
5
  export const CAPABILITY_SHAPE = Object.freeze({
8
6
  lifecycle: Object.freeze(["sessionStart", "sessionResume", "sessionEnd", "heartbeat",
9
7
  "childSessions"]),
@@ -20,6 +18,26 @@ const EVIDENCE_KEYS = new Set([...REQUIRED_TEXT, "limitations", "result"]);
20
18
  const RESULTS = new Set(["pass", "fail"]);
21
19
  const VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
22
20
  const PLATFORM = /^(?:darwin|linux|win32)-(?:arm64|x64)$/;
21
+ const TRIPLE = /^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/;
22
+
23
+ /** A client version as the triple that orders it. A prerelease is ordered by
24
+ * its release triple: 1.3.0-rc.1 is judged as 1.3.0, because a prerelease of a
25
+ * version ACC has already observed is not an older client. Anything unreadable
26
+ * - a probe that failed, "unknown", a vendor string - returns null.
27
+ */
28
+ function versionOrder(text) {
29
+ const match = typeof text === "string" && text.toLowerCase() !== "unknown"
30
+ ? TRIPLE.exec(text) : null;
31
+ return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
32
+ }
33
+
34
+ function compareVersionOrder(left, right) {
35
+ if (left === null || right === null) return left === right ? 0 : left === null ? -1 : 1;
36
+ for (let index = 0; index < 3; index += 1) {
37
+ if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
38
+ }
39
+ return 0;
40
+ }
23
41
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
24
42
  const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
25
43
 
@@ -144,29 +162,62 @@ function falseCapabilities() {
144
162
  * capability, is judged by the floor version's evidence for that capability.
145
163
  * Versions below the floor, prereleases and other platforms stay uncertified.
146
164
  */
147
- export function effectiveCapabilities(adapter, { clientVersion, platform } = {}) {
148
- const resolved = falseCapabilities();
149
- if (typeof clientVersion !== "string" || !VERSION.test(clientVersion)
150
- || clientVersion.toLowerCase() === "unknown"
151
- || typeof platform !== "string" || !PLATFORM.test(platform)
152
- || platform.toLowerCase() === "unknown") return freezeCapabilities(resolved);
165
+ /**
166
+ * Why a capability is on or off for one client, so a refusal can name the
167
+ * evidence that refused it instead of the version that asked.
168
+ *
169
+ * A capture says where a behaviour was observed, not which single release may
170
+ * use it. Clients ship almost daily, so evidence applies forward from the
171
+ * version that recorded it until a later capture changes that capability, and
172
+ * across platforms until one of them records something of its own. A version
173
+ * that cannot be read is judged by the newest evidence: a hook that runs has
174
+ * already proven the integration is installed.
175
+ *
176
+ * `reason` is one of `undeclared` (the adapter does not claim it), `unobserved`
177
+ * (nobody has captured it), `older-than-evidence` (this client predates the
178
+ * first capture, whose version is returned) or `recorded-failure` (a capture at
179
+ * the returned version recorded the loss).
180
+ */
181
+ export function capabilityEvidence(adapter, { clientVersion, platform } = {}, capability) {
182
+ const [group, name] = capability.split(".");
183
+ if (adapter.capabilities?.[group]?.[name] !== true) {
184
+ return { granted: false, reason: "undeclared", version: null };
185
+ }
153
186
  const client = adapter.client?.certificationName ?? adapter.client?.command;
154
- const rows = (adapter.certification?.evidence ?? [])
155
- .filter(item => item.client === client && item.platform === platform);
156
- const floor = adapter.certificationFloor?.[platform];
157
- const floored = typeof floor === "string" && parseStableVersion(floor) !== null
158
- && parseStableVersion(clientVersion) !== null
159
- && compareStableVersions(clientVersion, floor) >= 0;
160
- const certified = capability => {
161
- const own = rows.filter(item => item.version === clientVersion && item.capability === capability);
162
- const judged = own.length > 0 || !floored ? own
163
- : rows.filter(item => item.version === floor && item.capability === capability);
164
- return judged.some(item => item.result === "pass");
165
- };
187
+ const named = (adapter.certification?.evidence ?? [])
188
+ .filter(item => item.client === client && item.capability === capability);
189
+ if (named.length === 0) return { granted: false, reason: "unobserved", version: null };
190
+ const asked = versionOrder(clientVersion);
191
+ const reachable = asked === null ? named
192
+ : named.filter(item => compareVersionOrder(versionOrder(item.version), asked) <= 0);
193
+ // Version first, platform second: a loss recorded on one platform at 1.3.0
194
+ // says nothing about that platform at 1.2.3, where the only evidence in
195
+ // reach is another platform's passing capture.
196
+ const own = reachable.filter(item => item.platform === platform);
197
+ const usable = own.length > 0 ? own : reachable;
198
+ if (usable.length === 0) {
199
+ const first = named.reduce((earliest, item) =>
200
+ compareVersionOrder(versionOrder(item.version), versionOrder(earliest.version)) < 0
201
+ ? item : earliest);
202
+ return { granted: false, reason: "older-than-evidence", version: first.version };
203
+ }
204
+ const newest = usable.reduce((best, item) =>
205
+ compareVersionOrder(versionOrder(item.version), versionOrder(best.version)) > 0 ? item : best);
206
+ const deciding = versionOrder(newest.version);
207
+ // Two platforms can disagree at the deciding version when neither is the
208
+ // platform in hand. Withholding a body costs a trip to acc inbox; a false
209
+ // "delivered" loses the message, so a recorded loss wins the tie.
210
+ const granted = usable
211
+ .filter(item => compareVersionOrder(versionOrder(item.version), deciding) === 0)
212
+ .every(item => item.result === "pass");
213
+ return { granted, reason: granted ? null : "recorded-failure", version: newest.version };
214
+ }
215
+
216
+ export function effectiveCapabilities(adapter, facts = {}) {
217
+ const resolved = falseCapabilities();
166
218
  for (const [group, names] of Object.entries(CAPABILITY_SHAPE)) {
167
219
  for (const name of names) {
168
- resolved[group][name] = adapter.capabilities?.[group]?.[name] === true
169
- && certified(`${group}.${name}`);
220
+ resolved[group][name] = capabilityEvidence(adapter, facts, `${group}.${name}`).granted;
170
221
  }
171
222
  }
172
223
  return freezeCapabilities(resolved);
@@ -1,7 +1,8 @@
1
1
  // Capability contract, context projection, config ownership, and the binding
2
2
  // that survives between two ephemeral hook processes.
3
3
  export { CAPABILITY_SHAPE, assertCapabilities, defineAdapter } from "./capabilities.mjs";
4
- export { effectiveCapabilities, validateCertification } from "./certification.mjs";
4
+ export { capabilityEvidence, effectiveCapabilities, validateCertification }
5
+ from "./certification.mjs";
5
6
  export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
6
7
  compareStableVersions, evaluateNativeEligibility, evaluateVersionContract, parseStableVersion,
7
8
  validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -58,8 +58,7 @@ export function createSessionService(ports) {
58
58
  const ephemeral = await store.ephemeral.get("session", sessionId);
59
59
  const resolved = workspaceId ?? store.workspaceId ?? ephemeral?.workspaceId;
60
60
  if (resolved === undefined) return null;
61
- const durable = (await store.snapshot(resolved, { kinds: ["session"] })).sessions
62
- .find(session => session.sessionId === sessionId) ?? null;
61
+ const durable = await store.stateRecord(resolved, "session", sessionId);
63
62
  if (durable !== null) return { record: durable, durable: true };
64
63
  return ephemeral !== null && ephemeral.workspaceId === resolved
65
64
  ? { record: ephemeral, durable: false } : null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,7 +4,8 @@ import { readFile, realpath } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
- import { clearNativeAttempt, clearSessionBinding, effectiveCapabilities, loadSessionBinding, storeSessionBinding }
7
+ import { capabilityEvidence, clearNativeAttempt, clearSessionBinding, effectiveCapabilities,
8
+ loadSessionBinding, storeSessionBinding }
8
9
  from "@agents-can-communicate/adapter-sdk";
9
10
  import { createCoordinationService } from "@agents-can-communicate/core";
10
11
  import { AccError, createId } from "@agents-can-communicate/protocol";
@@ -301,12 +302,17 @@ async function projectTurn({ binding, context, adapter, adapterId,
301
302
  ? { text: await adapter.renderContext?.(projectionInput, projectionOptions) ?? "",
302
303
  offeredMessageIds: [], includedAttentionIds: [] }
303
304
  : await adapter.renderContextResult(projectionInput, projectionOptions);
304
- const clientFactsKnown = typeof binding.clientVersion === "string"
305
- && typeof binding.platform === "string";
305
+ // Name the evidence that refused, never the version that asked. "not
306
+ // certified" told the person their own client was at fault for existing;
307
+ // these two say what acc knows and what would change it.
308
+ const evidence = capabilityEvidence(adapter, binding, "delivery.nextTurn");
309
+ const refusal = {
310
+ "older-than-evidence": () => `client ${binding.clientVersion} is older than `
311
+ + `${evidence.version}, the first version acc verified for nextTurn`,
312
+ "recorded-failure": () => `acc recorded that nextTurn stopped working in ${evidence.version}`,
313
+ }[evidence.reason] ?? (() => "this client has no nextTurn evidence");
306
314
  const reason = !effective.delivery.nextTurn
307
- ? clientFactsKnown
308
- ? `client ${binding.clientVersion} on ${binding.platform} is not certified for nextTurn`
309
- : "the client version or platform is unknown"
315
+ ? refusal()
310
316
  : !hasStructuredRenderer ? "this adapter lacks structured delivery metadata" : null;
311
317
  const degradation = reason !== null && messages.length > 0
312
318
  ? `acc: ${messages.length} pending message(s) withheld because ${reason}; read `
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,7 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
 
4
- import { effectiveCapabilities, evaluateNativeEligibility, validateNativeActivationPlan }
4
+ import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility,
5
+ validateNativeActivationPlan }
5
6
  from "@agents-can-communicate/adapter-sdk";
6
7
 
7
8
  import { resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
@@ -186,9 +187,20 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
186
187
  } else if (typeof adapter.deliveryFallback?.diagnostic === "string") {
187
188
  const downgraded = adapter.capabilities?.delivery?.nextTurn === true
188
189
  && entry.capabilities?.delivery?.nextTurn !== true;
189
- entry.deliveryDiagnostic = (downgraded
190
- ? `${adapter.displayName} ${entry.version ?? "unknown version"} has no certified `
191
- + `next-turn delivery on ${platform}; ` : "") + adapter.deliveryFallback.diagnostic;
190
+ // Name the evidence, not the version in hand: "has no certified
191
+ // next-turn delivery" told people their own client was the problem,
192
+ // and left them nothing to act on.
193
+ const evidence = capabilityEvidence(adapter,
194
+ { clientVersion: entry.version, platform }, "delivery.nextTurn");
195
+ const named = entry.version ?? "unknown version";
196
+ const why = evidence.reason === "older-than-evidence"
197
+ ? `${adapter.displayName} ${named} is older than ${evidence.version}, the first `
198
+ + "version acc verified for next-turn delivery; "
199
+ : evidence.reason === "recorded-failure"
200
+ ? `acc recorded that ${adapter.displayName} next-turn delivery stopped working `
201
+ + `in ${evidence.version}; `
202
+ : `${adapter.displayName} next-turn delivery has not been captured; `;
203
+ entry.deliveryDiagnostic = (downgraded ? why : "") + adapter.deliveryFallback.diagnostic;
192
204
  }
193
205
  if (entry.deliveryDiagnostic !== null) entry.diagnostics.push(entry.deliveryDiagnostic);
194
206
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -58,28 +58,34 @@ function ephemeralDirectory(paths, kind, id) {
58
58
  return path.join(paths.retained, "ephemeral", kind, id);
59
59
  }
60
60
 
61
+ // The newest marker decides a record's state, so only that one is read and
62
+ // checked. Every name is still validated first - an unparsable sequence fails
63
+ // closed before anything is selected - and names are ordered as numbers, since
64
+ // a sequence can outgrow its padding. Reading every marker made each lookup
65
+ // cost an open and a full ancestor check per marker ever written: thousands
66
+ // for a delivery binding renewed every forty seconds, and seconds for one
67
+ // status in a workspace that had been in use for weeks.
61
68
  async function latestEphemeralMarker(paths, root, kind, id) {
62
69
  const directory = ephemeralDirectory(paths, kind, id);
63
- const markers = [];
64
- for (const filePath of await listJsonFiles(directory, { root })) {
70
+ const named = (await listJsonFiles(directory, { root })).map(filePath => {
65
71
  const sequence = path.basename(filePath, ".json");
72
+ if (typeof sequence !== "string" || !/^\d+$/.test(sequence)
73
+ || BigInt(sequence) < 1n || sequence !== pad(BigInt(sequence))) {
74
+ data("invalid ephemeral retention marker", { filePath });
75
+ }
76
+ return { filePath, sequence, order: BigInt(sequence) };
77
+ }).sort((left, right) => (left.order < right.order ? 1 : left.order > right.order ? -1 : 0));
78
+ for (const { filePath, sequence } of named) {
66
79
  const found = await readJsonIfPresent(filePath, root);
67
80
  if (found === null) continue;
68
81
  const marker = found.value;
69
- if (typeof sequence !== "string" || !/^\d+$/.test(sequence)
70
- || BigInt(sequence) < 1n || sequence !== pad(BigInt(sequence))
71
- || !(marker?.state === "present" || marker?.state === "deleted")) {
82
+ if (!(marker?.state === "present" || marker?.state === "deleted")) {
72
83
  data("invalid ephemeral retention marker", { filePath });
73
84
  }
74
- assertMarker(found, { area: "ephemeral", kind, id, sequence, state: marker.state },
75
- filePath);
76
- markers.push(marker);
85
+ assertMarker(found, { area: "ephemeral", kind, id, sequence, state: marker.state }, filePath);
86
+ return marker;
77
87
  }
78
- return markers.sort((left, right) => {
79
- const leftSequence = BigInt(left.sequence);
80
- const rightSequence = BigInt(right.sequence);
81
- return leftSequence < rightSequence ? -1 : leftSequence > rightSequence ? 1 : 0;
82
- }).at(-1) ?? null;
88
+ return null;
83
89
  }
84
90
 
85
91
  export async function ephemeralIsDeleted(paths, root, kind, id) {
@@ -91,6 +97,10 @@ export async function markEphemeral(paths, options, kind, id, state) {
91
97
  data("invalid ephemeral retention state", { state });
92
98
  }
93
99
  const previous = await latestEphemeralMarker(paths, options.root, kind, id);
100
+ // A marker that repeats the current state changes nothing, and every renewal
101
+ // of a live record used to append one. History now grows only with a real
102
+ // change: published after a deletion, or deleted.
103
+ if (previous?.state === state) return previous;
94
104
  const sequence = pad(previous === null ? 1n : BigInt(previous.sequence) + 1n);
95
105
  const record = { retentionVersion: RETENTION_VERSION, area: "ephemeral", kind, id,
96
106
  sequence, state };
@@ -265,6 +265,23 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
265
265
  * cost grow with the number of messages the workspace had ever carried, and
266
266
  * the hook budget is five seconds after which it allows the write.
267
267
  */
268
+ /**
269
+ * One state record by id, or null - the checks a listing applies to each
270
+ * record (the path names it, a deleted generation is absent, the record
271
+ * validates, it belongs to this workspace) without reading every other
272
+ * record of its kind. Looking one session up by listing them all cost a full
273
+ * pass over every session the workspace ever had, several times a hook.
274
+ */
275
+ async function stateRecord(workspace, kind, id) {
276
+ const filePath = statePath(paths, kind, id);
277
+ const found = await readJsonIfPresent(filePath, root);
278
+ if (found === null) return null;
279
+ const envelope = assertStateBinding(found.value, kind, id, filePath);
280
+ if (await stateGenerationIsDeleted(paths, root, kind, envelope.id, envelope.generation)) return null;
281
+ validateRecord(kind, envelope.record);
282
+ return envelope.record.workspaceId === workspace ? envelope.record : null;
283
+ }
284
+
268
285
  async function snapshot(workspace, { kinds } = {}) {
269
286
  const wanted = kinds === undefined ? null : new Set(kinds);
270
287
  const of = async kind => {
@@ -348,6 +365,6 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
348
365
  },
349
366
  });
350
367
 
351
- return Object.freeze({ transaction, eventsSince, snapshot, ephemeral, paths, root,
368
+ return Object.freeze({ transaction, eventsSince, snapshot, stateRecord, ephemeral, paths, root,
352
369
  workspaceId });
353
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "accManagedUpdateProtocol": 2,
5
5
  "accStoreVersion": 6,
6
6
  "type": "module",