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.
Files changed (155) hide show
  1. package/README.md +87 -69
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-bootstrap.mjs +56 -0
  4. package/bin/acc-claude-channel.mjs +177 -0
  5. package/bin/acc-hook.mjs +94 -12
  6. package/bin/acc-mcp.mjs +6 -2
  7. package/bin/acc.mjs +13 -3
  8. package/docs/ADAPTER_AUTHORING.md +204 -0
  9. package/docs/ARCHITECTURE.md +131 -0
  10. package/docs/CAPABILITIES.md +117 -214
  11. package/docs/CLI.md +164 -0
  12. package/docs/CONCEPTS.md +134 -0
  13. package/docs/CONFIGURATION.md +147 -0
  14. package/docs/DESIGN_DECISIONS.md +89 -0
  15. package/docs/GETTING_STARTED.md +145 -0
  16. package/docs/GLOSSARY.md +26 -0
  17. package/docs/HOW_IT_WORKS.md +277 -0
  18. package/docs/MCP.md +94 -0
  19. package/docs/PROTOCOL.md +200 -0
  20. package/docs/RELEASING.md +115 -0
  21. package/docs/SECURITY_MODEL.md +131 -0
  22. package/docs/TROUBLESHOOTING.md +108 -0
  23. package/docs/WHY_ACC.md +61 -0
  24. package/docs/index.md +44 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +269 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -2
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +22 -22
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +150 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +199 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/package.json +11 -2
  51. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +22 -22
  53. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
  57. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +68 -0
  58. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +22 -22
  60. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
  61. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -0
  66. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  67. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +31 -13
  68. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  69. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  70. package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
  71. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
  72. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
  73. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  74. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  75. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  76. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  77. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  78. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  79. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  80. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  81. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +22 -22
  82. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
  83. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  84. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -18
  85. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  86. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
  87. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
  88. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -2
  89. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
  90. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
  91. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
  92. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +28 -4
  93. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
  95. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
  96. package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
  97. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
  98. package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
  99. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  101. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  102. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +131 -0
  103. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  104. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
  105. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
  106. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  107. package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
  108. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  109. package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
  110. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
  111. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  112. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
  113. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  114. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  115. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +131 -0
  116. package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
  117. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  118. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
  119. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +190 -105
  120. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  121. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
  122. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
  123. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
  124. package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
  125. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
  126. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
  127. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
  128. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
  129. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  130. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  131. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  132. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
  133. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
  134. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  135. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  137. package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
  138. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  139. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -90
  140. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  141. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  143. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  144. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  145. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  146. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  147. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  148. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
  149. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
  150. package/package.json +20 -1
  151. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  152. package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
  153. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  154. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  155. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
@@ -1,134 +1,118 @@
1
- import { AccError, EXIT, SCHEMA_VERSION, advanceDelivery, createId, validateRecord }
1
+ import { AccError, EXIT, SCHEMA_VERSION, advanceReceipt }
2
2
  from "@agents-can-communicate/protocol";
3
3
 
4
- const receiptId = (messageId, recipient) => `${messageId}--${recipient}`;
5
- const unresolved = (message, receipt) => receipt.state === "recorded"
6
- || receipt.state === "queued"
7
- || (message.requiresAck && (receipt.state === "injected" || receipt.state === "seen"));
4
+ import { receiptId, recordMessageInTransaction } from "./conversations.mjs";
5
+
6
+ const listable = (message, receipt) => receipt.state === "queued" || receipt.state === "offered"
7
+ || (receipt.state === "retrieved" && message.obligation !== "none");
8
8
 
9
- /** Narrow, recipient-owned message reads and replies. */
10
9
  export function createInboxService(ports, sessions) {
11
10
  const { store, clock, ids } = ports;
12
11
 
13
12
  async function requireOpen(input, action) {
14
- const existing = await sessions.locateSession(input.sessionId, input.workspaceId);
15
- if (existing === null || existing.record.state !== "open"
16
- || existing.record.generation !== input.generation) {
13
+ const located = await sessions.locateSession(input.sessionId, input.workspaceId);
14
+ if (located === null || located.record.state !== "open"
15
+ || located.record.generation !== input.generation) {
17
16
  throw new AccError(EXIT.CONFLICT, `cannot ${action} from this session generation`,
18
17
  { sessionId: input.sessionId });
19
18
  }
20
- return existing.record;
19
+ return located.record;
20
+ }
21
+
22
+ function requireOwnedReceipt(tx, session, messageId) {
23
+ const message = tx.get("message", messageId);
24
+ const id = receiptId(messageId, session.participantId);
25
+ const receipt = tx.get("receipt", id);
26
+ if (message === null || receipt === null) {
27
+ throw new AccError(EXIT.CONFLICT,
28
+ "that message is not addressed to this participant",
29
+ { messageId, participantId: session.participantId });
30
+ }
31
+ return { id, message, receipt };
32
+ }
33
+
34
+ function advanceOwned(tx, session, messageId, state, now) {
35
+ const owned = requireOwnedReceipt(tx, session, messageId);
36
+ const nextState = advanceReceipt(owned.receipt.state, state);
37
+ if (nextState === owned.receipt.state) {
38
+ return { message: owned.message, receipt: owned.receipt };
39
+ }
40
+ const receipt = { ...owned.receipt, state: nextState, updatedAt: now };
41
+ tx.put("receipt", owned.id, receipt, tx.generationOf("receipt", owned.id));
42
+ tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
43
+ workspaceId: session.workspaceId, actorSessionId: session.sessionId,
44
+ type: `message.${state}`, occurredAt: now,
45
+ payload: { messageId, recipientParticipantId: session.participantId } });
46
+ return { message: owned.message, receipt };
21
47
  }
22
48
 
23
49
  async function readInbox(input) {
24
50
  const session = await requireOpen(input, "read the inbox");
25
- const snapshot = await store.snapshot(session.workspaceId,
26
- { kinds: ["message", "receipt"] });
27
- const messages = new Map(snapshot.messages.map(message => [message.messageId, message]));
28
- let addressed = snapshot.receipts
29
- .filter(receipt => receipt.recipientParticipantId === session.participantId)
30
- .map(receipt => ({ message: messages.get(receipt.messageId), receipt }))
31
- .filter(item => item.message !== undefined);
32
- if (input.messageId !== undefined) {
33
- addressed = addressed.filter(item => item.message.messageId === input.messageId
34
- && item.receipt.state !== "acknowledged" && item.receipt.state !== "failed");
35
- if (addressed.length === 0) {
36
- throw new AccError(EXIT.CONFLICT,
37
- "that message is not recoverable by this participant",
38
- { messageId: input.messageId, participantId: session.participantId });
51
+ const now = clock.now();
52
+ return store.transaction(tx => {
53
+ const messages = new Map(tx.list("message").map(item => [item.messageId, item]));
54
+ let selected = tx.list("receipt", receipt =>
55
+ receipt.recipientParticipantId === session.participantId)
56
+ .filter(receipt => messages.has(receipt.messageId));
57
+ if (input.messageId === undefined) {
58
+ selected = selected.filter(receipt => listable(messages.get(receipt.messageId), receipt));
59
+ } else {
60
+ selected = selected.filter(receipt => receipt.messageId === input.messageId
61
+ && receipt.state !== "acknowledged");
62
+ if (selected.length === 0) {
63
+ throw new AccError(EXIT.CONFLICT,
64
+ "that message is not recoverable by this participant",
65
+ { messageId: input.messageId, participantId: session.participantId });
66
+ }
39
67
  }
40
- } else {
41
- addressed = addressed.filter(item => unresolved(item.message, item.receipt));
42
- }
43
- addressed.sort((left, right) => left.message.sentAt.localeCompare(right.message.sentAt)
44
- || left.message.messageId.localeCompare(right.message.messageId));
45
- if (addressed.length === 0) return [];
68
+ selected.sort((left, right) => {
69
+ const a = messages.get(left.messageId);
70
+ const b = messages.get(right.messageId);
71
+ return a.sentAt.localeCompare(b.sentAt) || a.messageId.localeCompare(b.messageId);
72
+ });
73
+ return selected.map(receipt => advanceOwned(tx, session, receipt.messageId,
74
+ "retrieved", now));
75
+ }, { kinds: ["message", "receipt"] });
76
+ }
46
77
 
78
+ async function acknowledgeMessage(input) {
79
+ const session = await requireOpen(input, "acknowledge a message");
47
80
  const now = clock.now();
48
- const shown = [];
49
- await store.transaction(async tx => {
50
- for (const item of addressed) {
51
- const id = receiptId(item.message.messageId, session.participantId);
52
- const current = tx.get("receipt", id);
53
- const receipt = { ...current, state: advanceDelivery(current.state, "seen"),
54
- updatedAt: now };
55
- tx.put("receipt", id, receipt, tx.generationOf("receipt", id));
56
- tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
57
- workspaceId: session.workspaceId, actorSessionId: session.sessionId,
58
- type: "message.seen", occurredAt: now,
59
- payload: { messageId: item.message.messageId,
60
- recipientParticipantId: session.participantId } });
61
- shown.push({ message: item.message, receipt });
62
- }
63
- }, { kinds: ["receipt"] });
64
- return shown;
81
+ return store.transaction(tx => advanceOwned(tx, session, input.messageId,
82
+ "acknowledged", now).receipt,
83
+ { kinds: ["message", "receipt"] });
65
84
  }
66
85
 
67
86
  async function replyToMessage(input) {
68
87
  const session = await requireOpen(input, "reply to a message");
69
88
  const now = clock.now();
70
- const replyId = createId("message");
71
- let result = null;
72
- await store.transaction(async tx => {
73
- const original = tx.get("message", input.messageId);
74
- const ownReceiptId = receiptId(input.messageId, session.participantId);
75
- const originalReceipt = tx.get("receipt", ownReceiptId);
76
- if (original === null || originalReceipt === null
77
- || !original.toParticipantIds.includes(session.participantId)) {
78
- throw new AccError(EXIT.CONFLICT,
79
- "that message is not addressed to this participant",
80
- { messageId: input.messageId, participantId: session.participantId });
81
- }
82
- if (originalReceipt.state === "acknowledged" || originalReceipt.state === "failed") {
89
+ const replyId = ids.next("message");
90
+ return store.transaction(tx => {
91
+ const original = requireOwnedReceipt(tx, session, input.messageId);
92
+ const matchingReply = tx.list("message", message =>
93
+ message.workspaceId === session.workspaceId
94
+ && message.fromParticipantId === session.participantId
95
+ && message.clientMessageId === input.clientMessageId).at(0);
96
+ if (original.receipt.state === "acknowledged" && matchingReply === undefined) {
83
97
  throw new AccError(EXIT.CONFLICT, "that message is already resolved",
84
- { messageId: input.messageId, state: originalReceipt.state });
98
+ { messageId: input.messageId });
85
99
  }
86
- const reply = validateRecord("message", {
87
- schemaVersion: SCHEMA_VERSION,
88
- messageId: replyId,
89
- workspaceId: session.workspaceId,
90
- fromSessionId: session.sessionId,
91
- fromParticipantId: session.participantId,
92
- toParticipantIds: [original.fromParticipantId],
93
- type: input.type ?? "answer",
94
- subject: input.subject ?? `Re: ${original.subject}`,
95
- body: input.body,
96
- priority: input.priority ?? "normal",
97
- workstreamId: original.workstreamId,
98
- taskId: original.taskId,
99
- inReplyTo: original.messageId,
100
- requiresAck: false,
101
- artifacts: input.artifacts ?? [],
102
- sentAt: now,
103
- });
104
- const acknowledged = { ...originalReceipt,
105
- state: advanceDelivery(originalReceipt.state, "acknowledged"), updatedAt: now };
106
- const replyReceipt = validateRecord("receipt", {
107
- schemaVersion: SCHEMA_VERSION,
108
- messageId: replyId,
109
- workspaceId: session.workspaceId,
110
- recipientParticipantId: original.fromParticipantId,
111
- state: "queued",
112
- updatedAt: now,
113
- });
114
- tx.put("message", replyId, reply);
115
- tx.put("receipt", receiptId(replyId, original.fromParticipantId), replyReceipt);
116
- tx.put("receipt", ownReceiptId, acknowledged,
117
- tx.generationOf("receipt", ownReceiptId));
118
- tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
119
- workspaceId: session.workspaceId, actorSessionId: session.sessionId,
120
- type: "message.sent", occurredAt: now,
121
- payload: { messageId: replyId, type: reply.type,
122
- recipients: reply.toParticipantIds } });
123
- tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
124
- workspaceId: session.workspaceId, actorSessionId: session.sessionId,
125
- type: "message.acknowledged", occurredAt: now,
126
- payload: { messageId: original.messageId,
127
- recipientParticipantId: session.participantId } });
128
- result = { reply, receipt: acknowledged };
129
- }, { kinds: ["message", "receipt"] });
130
- return result;
100
+ const recorded = recordMessageInTransaction({ tx, session, now, messageId: replyId, ids,
101
+ action: "reply to a message", input: {
102
+ clientMessageId: input.clientMessageId,
103
+ toParticipantIds: [original.message.fromParticipantId],
104
+ kind: "answer",
105
+ obligation: "none",
106
+ subject: input.subject ?? `Re: ${original.message.subject}`,
107
+ body: input.body,
108
+ inReplyTo: original.message.messageId,
109
+ artifacts: input.artifacts ?? [],
110
+ handoff: null,
111
+ } });
112
+ const receipt = advanceOwned(tx, session, input.messageId, "acknowledged", now).receipt;
113
+ return { reply: recorded.message, receipt };
114
+ }, { kinds: ["participant", "session", "message", "receipt"] });
131
115
  }
132
116
 
133
- return { readInbox, replyToMessage };
117
+ return { readInbox, replyToMessage, acknowledgeMessage };
134
118
  }
@@ -3,7 +3,7 @@ export { createCoordinationService } from "./service.mjs";
3
3
  export { assertPorts } from "./ports.mjs";
4
4
  export { classifySessionPresence } from "./sessions.mjs";
5
5
  export { defaultPidIsAlive } from "./pid.mjs";
6
- export { ATTENTION_PRIORITY, computeAttention } from "./sync.mjs";
7
- export { looksConsequential, noteNudge } from "./message-signals.mjs";
6
+ export { ATTENTION_PRIORITY, computeAttention } from "./attention.mjs";
7
+ export { SAFE_OFFER_ERROR_CODES } from "./receipts.mjs";
8
+ export { createDeliveryBindingService } from "./delivery-bindings.mjs";
8
9
  export { overlaps } from "./claims.mjs";
9
- export { wouldCycle } from "./tasks.mjs";
@@ -35,7 +35,6 @@ export function createIntentService(ports, sessions) {
35
35
  summary: input.summary,
36
36
  mode: input.mode,
37
37
  resourceHints: input.resourceHints ?? [],
38
- workstreamId: input.workstreamId ?? null,
39
38
  state: input.state ?? "active",
40
39
  updatedAt: now,
41
40
  });
@@ -8,7 +8,8 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
8
8
  * list(kind: string, predicate?: (record: object) => boolean): object[],
9
9
  * put(kind: string, id: string, record: object, expectedGeneration?: string | null): string,
10
10
  * append(event: object): object }} CoordinationTransaction
11
- * @typedef {{ transaction(callback: (tx: CoordinationTransaction) => unknown): Promise<unknown>,
11
+ * @typedef {{ transaction(callback: (tx: CoordinationTransaction) => unknown,
12
+ * options?: { kinds?: string[], deadlineAt?: number }): Promise<unknown>,
12
13
  * eventsSince(workspaceId: string, cursor: string | null, limit: number): Promise<object>,
13
14
  * snapshot(workspaceId: string): Promise<object> }} CoordinationStore
14
15
  */
@@ -0,0 +1,109 @@
1
+ import { AccError, EXIT, SCHEMA_VERSION, advanceReceipt }
2
+ from "@agents-can-communicate/protocol";
3
+
4
+ import { receiptId } from "./conversations.mjs";
5
+
6
+ export const SAFE_OFFER_ERROR_CODES = Object.freeze([
7
+ "ambiguous_recipient_sessions",
8
+ "delivery_disabled",
9
+ "recipient_busy",
10
+ "recipient_unavailable",
11
+ "transport_error",
12
+ "transport_rejected",
13
+ "unsupported_client_version",
14
+ ]);
15
+
16
+ function missingReceipt(messageId, recipientParticipantId) {
17
+ return new AccError(EXIT.CONFLICT, "no receipt exists for that recipient",
18
+ { messageId, recipientParticipantId });
19
+ }
20
+
21
+ function requireReceipt(tx, messageId, recipientParticipantId) {
22
+ const receipt = tx.get("receipt", receiptId(messageId, recipientParticipantId));
23
+ if (receipt === null) throw missingReceipt(messageId, recipientParticipantId);
24
+ const message = tx.get("message", messageId);
25
+ if (message === null) throw missingReceipt(messageId, recipientParticipantId);
26
+ return { message, receipt };
27
+ }
28
+
29
+ function requireOfferableReceipt(tx, input, { allowRoomNextTurn }) {
30
+ const found = requireReceipt(tx, input.messageId, input.recipientParticipantId);
31
+ if (found.message.toParticipantIds.length === 0
32
+ && (!allowRoomNextTurn || input.transport !== "next-turn")) {
33
+ throw new AccError(EXIT.CONFLICT, "room messages are not eligible for live offers",
34
+ { messageId: input.messageId, recipientParticipantId: input.recipientParticipantId });
35
+ }
36
+ return found.receipt;
37
+ }
38
+
39
+ function requireTarget(tx, input, { mustBeOpen }) {
40
+ const target = tx.get("session", input.targetSessionId);
41
+ if (target === null || (mustBeOpen && target.state !== "open")
42
+ || target.participantId !== input.recipientParticipantId
43
+ || target.generation !== input.targetGeneration) {
44
+ throw new AccError(EXIT.CONFLICT,
45
+ "the offer target is not this recipient's session generation",
46
+ { targetSessionId: input.targetSessionId,
47
+ recipientParticipantId: input.recipientParticipantId });
48
+ }
49
+ return target;
50
+ }
51
+
52
+ export function createReceiptService(ports) {
53
+ const { store, clock, ids } = ports;
54
+
55
+ async function readReceipt(input) {
56
+ return store.transaction(tx => requireReceipt(tx, input.messageId,
57
+ input.recipientParticipantId).receipt, { kinds: ["message", "receipt"] });
58
+ }
59
+
60
+ async function recordOfferSucceeded(input) {
61
+ const now = clock.now();
62
+ return store.transaction(tx => {
63
+ const id = receiptId(input.messageId, input.recipientParticipantId);
64
+ const receipt = requireOfferableReceipt(tx, input, { allowRoomNextTurn: true });
65
+ const target = requireTarget(tx, input, { mustBeOpen: true });
66
+ // A retrieval or acknowledgement may win after a transport accepted but
67
+ // before this transaction acquired the writer. That stronger truth must
68
+ // remain in place. The same rule makes a repeated successful commit a
69
+ // read-only operation rather than another success event.
70
+ if (receipt.state !== "queued") return receipt;
71
+ const offered = { ...receipt, state: advanceReceipt(receipt.state, "offered"),
72
+ updatedAt: now };
73
+ tx.put("receipt", id, offered, tx.generationOf("receipt", id));
74
+ tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
75
+ workspaceId: receipt.workspaceId, actorSessionId: target.sessionId,
76
+ type: "message.offer_succeeded", occurredAt: now,
77
+ payload: { messageId: input.messageId,
78
+ recipientParticipantId: input.recipientParticipantId,
79
+ targetSessionId: input.targetSessionId,
80
+ targetGeneration: input.targetGeneration,
81
+ transport: input.transport, adapterId: input.adapterId,
82
+ clientVersion: input.clientVersion } });
83
+ return offered;
84
+ }, { kinds: ["session", "message", "receipt"], deadlineAt: input.deadlineAt });
85
+ }
86
+
87
+ async function recordOfferFailed(input) {
88
+ if (!SAFE_OFFER_ERROR_CODES.includes(input.safeErrorCode)) {
89
+ throw new AccError(EXIT.DATA, "safeErrorCode is not in the closed offer error set",
90
+ { safeErrorCode: input.safeErrorCode });
91
+ }
92
+ const now = clock.now();
93
+ return store.transaction(tx => {
94
+ const receipt = requireOfferableReceipt(tx, input, { allowRoomNextTurn: false });
95
+ const target = requireTarget(tx, input, { mustBeOpen: false });
96
+ return tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
97
+ workspaceId: receipt.workspaceId, actorSessionId: target.sessionId,
98
+ type: "message.offer_failed", occurredAt: now,
99
+ payload: { messageId: input.messageId,
100
+ recipientParticipantId: input.recipientParticipantId,
101
+ targetSessionId: target.sessionId,
102
+ targetGeneration: target.generation,
103
+ transport: input.transport, adapterId: input.adapterId,
104
+ clientVersion: input.clientVersion, safeErrorCode: input.safeErrorCode } });
105
+ }, { kinds: ["session", "message", "receipt"] });
106
+ }
107
+
108
+ return { readReceipt, recordOfferSucceeded, recordOfferFailed };
109
+ }
@@ -1,14 +1,14 @@
1
1
  import { createClaimService } from "./claims.mjs";
2
- import { createCommunicationService } from "./communication.mjs";
2
+ import { createConversationService } from "./conversations.mjs";
3
+ import { createDeliveryBindingService } from "./delivery-bindings.mjs";
3
4
  import { createIntentService } from "./intents.mjs";
4
5
  import { createInboxService } from "./inbox.mjs";
5
6
  import { defaultPidIsAlive } from "./pid.mjs";
6
7
  import { assertPorts } from "./ports.mjs";
8
+ import { createReceiptService } from "./receipts.mjs";
7
9
  import { createSessionService } from "./sessions.mjs";
8
10
  import { createGuardStateService, createStatusService } from "./status.mjs";
9
11
  import { createSyncService } from "./sync.mjs";
10
- import { createTaskService } from "./tasks.mjs";
11
- import { createWorkstreamService } from "./workstreams.mjs";
12
12
 
13
13
  /**
14
14
  * Composition root for the domain services. Everything time- or
@@ -29,13 +29,13 @@ export function createCoordinationService({ store, clock, ids,
29
29
  const ports = assertPorts({ store, clock, ids, pidIsAlive });
30
30
  const sessions = createSessionService(ports);
31
31
  const intents = createIntentService(ports, sessions);
32
- const workstreams = createWorkstreamService(ports, sessions);
33
- const tasks = createTaskService(ports, workstreams);
34
32
  const claims = createClaimService(ports, sessions);
35
- const communication = createCommunicationService(ports, sessions, claims);
33
+ const conversations = createConversationService(ports, sessions);
34
+ const deliveryBindings = createDeliveryBindingService(ports, sessions);
36
35
  const inbox = createInboxService(ports, sessions);
36
+ const receipts = createReceiptService(ports);
37
37
  const sync = createSyncService(ports, sessions);
38
- const status = createStatusService(ports, sessions);
38
+ const status = createStatusService(ports, sessions, deliveryBindings);
39
39
  const guardState = createGuardStateService(ports);
40
40
  return Object.freeze({
41
41
  store,
@@ -43,12 +43,23 @@ export function createCoordinationService({ store, clock, ids,
43
43
  ids,
44
44
  policies: Object.freeze({ ...policies }),
45
45
  ...sessions,
46
+ // Closing a session also retires its live endpoint under the same
47
+ // generation, so nothing can be offered into a session that has left.
48
+ closeSession: async input => {
49
+ const closed = await sessions.closeSession(input);
50
+ await deliveryBindings.clearDeliveryBinding({ sessionId: input.sessionId,
51
+ generation: input.generation });
52
+ return closed;
53
+ },
46
54
  ...intents,
47
- ...workstreams,
48
- ...tasks,
49
55
  ...claims,
50
- ...communication,
56
+ ...conversations,
57
+ publishDeliveryBinding: deliveryBindings.publishDeliveryBinding,
58
+ clearDeliveryBinding: deliveryBindings.clearDeliveryBinding,
59
+ listDeliveryBindings: deliveryBindings.listDeliveryBindings,
60
+ refreshDeliveryBinding: deliveryBindings.refreshDeliveryBinding,
51
61
  ...inbox,
62
+ ...receipts,
52
63
  ...sync,
53
64
  ...status,
54
65
  guardState,
@@ -2,7 +2,6 @@ import { AccError, EXIT, SCHEMA_VERSION, assertPortableId, createId, validateRec
2
2
  from "@agents-can-communicate/protocol";
3
3
 
4
4
  import { ensureMaterialised, isMaterialised, materialise } from "./materialisation.mjs";
5
- import { writeWorkResponse } from "./notify.mjs";
6
5
 
7
6
  // A hook-only adapter heartbeats only when its harness gives it a turn, so the
8
7
  // staleness window is a multiple of the cadence the session itself declared
@@ -154,7 +153,12 @@ export function createSessionService(ports) {
154
153
  // The approved trigger is the SECOND live session, not the first: a lone
155
154
  // session must be able to open and close without leaving a trace.
156
155
  const live = (await store.ephemeral.list("session")).filter(item => item.state === "open");
157
- if (live.length > 1) {
156
+ // Recheck durable state only after our ephemeral writes. Another process
157
+ // may have staged the old ephemeral set after our first precheck, committed
158
+ // materialisation, and retired that set before these puts completed. In
159
+ // that case this attach is the only ephemeral session left, but it still
160
+ // has to run the idempotent promotion pass into the now-durable workspace.
161
+ if (live.length > 1 || await isMaterialised(store, workspaceId)) {
158
162
  await materialise(ports, { workspaceId, descriptor: input.descriptor,
159
163
  reason: "second_live_session" });
160
164
  }
@@ -243,35 +247,33 @@ export function createSessionService(ports) {
243
247
 
244
248
  await store.transaction(async tx => {
245
249
  tx.put("session", sessionId, closed, tx.generationOf("session", sessionId));
246
- // Work in progress goes back on the table. A task held by a session that
247
- // has gone stayed `in_progress` forever, nobody else could take it, and
248
- // whoever asked for it was never told - a request handed to an agent that
249
- // closed its terminal simply vanished.
250
- for (const task of tx.list("task")) {
251
- if (task.assigneeSessionId !== sessionId) continue;
252
- if (task.state === "done") continue;
253
- const released = { ...task, assigneeSessionId: null, state: "pending" };
254
- tx.put("task", task.taskId, released, tx.generationOf("task", task.taskId));
255
- writeWorkResponse(tx, { task, actor: closed, workspaceId: closed.workspaceId,
256
- now, ids, outcome: "released",
257
- reason: "the session working on this closed before finishing it" });
258
- tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
259
- workspaceId: closed.workspaceId, actorSessionId: sessionId,
260
- type: "task.released", occurredAt: now, payload: { taskId: task.taskId } });
261
- }
262
250
  tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
263
251
  workspaceId: closed.workspaceId, actorSessionId: sessionId, type: "session.closed",
264
252
  occurredAt: now, payload: {} });
265
- // `writeWorkResponse` tells whoever asked, on this same handle.
266
- }, { kinds: ["message", "receipt", "session", "task"] });
253
+ }, { kinds: ["session"] });
267
254
  return closed;
268
255
  }
269
256
 
257
+ async function listLiveSessions({ participantId, workspaceId, now = clock.now() }) {
258
+ const resolved = workspaceId ?? store.workspaceId;
259
+ const snapshot = resolved === undefined ? null
260
+ : await store.snapshot(resolved, { kinds: ["workspace", "session"] });
261
+ const records = snapshot?.workspace === null
262
+ ? await store.ephemeral.list("session")
263
+ : snapshot?.sessions ?? await store.ephemeral.list("session");
264
+ return records
265
+ .filter(session => session.participantId === participantId
266
+ && classifySessionPresence(session, now, pidIsAlive) !== "offline")
267
+ .sort((left, right) => left.sessionId.localeCompare(right.sessionId))
268
+ .map(session => ({ sessionId: session.sessionId, generation: session.generation }));
269
+ }
270
+
270
271
  return {
271
272
  openSession,
272
273
  resumeSession,
273
274
  heartbeatSession,
274
275
  closeSession,
276
+ listLiveSessions,
275
277
  locateSession: locate,
276
278
  ensureMaterialised: options => ensureMaterialised(ports, options),
277
279
  };
@@ -1,5 +1,5 @@
1
1
  import { classifySessionPresence } from "./sessions.mjs";
2
- import { computeAttention } from "./sync.mjs";
2
+ import { computeAttention } from "./attention.mjs";
3
3
 
4
4
  /**
5
5
  * Protection level, reported from what is actually enforceable.
@@ -57,7 +57,7 @@ export function createGuardStateService(ports) {
57
57
  };
58
58
  }
59
59
 
60
- export function createStatusService(ports, sessions) {
60
+ export function createStatusService(ports, sessions, deliveryBindings) {
61
61
  const { store, clock, pidIsAlive } = ports;
62
62
 
63
63
  async function collectStatus(input = {}) {
@@ -85,6 +85,7 @@ export function createStatusService(ports, sessions) {
85
85
  const live = classified.filter(item => item.presence !== "offline");
86
86
  const claims = snapshot.claims
87
87
  .filter(claim => Date.parse(claim.expiresAt) > Date.parse(now));
88
+ const currentBindings = await deliveryBindings.currentBindings(now);
88
89
 
89
90
  return {
90
91
  workspaceId,
@@ -109,12 +110,6 @@ export function createStatusService(ports, sessions) {
109
110
  presence,
110
111
  intent: intents.find(intent => intent.sessionId === session.sessionId)?.summary ?? null,
111
112
  })),
112
- workstreams: snapshot.workstreams.map(workstream => ({
113
- workstreamId: workstream.workstreamId,
114
- title: workstream.title,
115
- state: workstream.state,
116
- coordinatorSessionId: workstream.coordinatorSessionId,
117
- })),
118
113
  // The owner is named twice on purpose. Every command that reaches a peer
119
114
  // takes a participant id, so a claim that gave only a session id sent the
120
115
  // reader back through the roster to answer "who is holding this, and how
@@ -133,13 +128,20 @@ export function createStatusService(ports, sessions) {
133
128
  .find(session => session.sessionId === claim.ownerSessionId)?.participantId ?? null,
134
129
  expiresAt: claim.expiresAt,
135
130
  })),
131
+ deliveryBindings: currentBindings.map(({ binding, reachable }) => ({
132
+ adapterId: binding.adapterId,
133
+ clientVersion: binding.clientVersion,
134
+ availableModes: [...binding.availableModes],
135
+ livePolicy: binding.livePolicy,
136
+ reachable,
137
+ leaseUntil: binding.leaseUntil,
138
+ })),
136
139
  attention: computeAttention(snapshot, { session: null,
137
140
  participantId: input.participantId, now, pidIsAlive }),
138
141
  counts: {
139
142
  live: live.length,
140
143
  stale: live.filter(item => item.presence === "stale").length,
141
144
  claims: claims.length,
142
- tasks: snapshot.tasks.length,
143
145
  messages: snapshot.messages.length,
144
146
  },
145
147
  };