agents-can-communicate 0.3.1 → 0.4.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 (157) hide show
  1. package/README.md +28 -7
  2. package/SECURITY.md +1 -1
  3. package/bin/acc-bootstrap.mjs +5 -54
  4. package/bin/acc-claude-channel.mjs +8 -175
  5. package/bin/acc-hook.mjs +4 -133
  6. package/bin/acc-mcp.mjs +4 -59
  7. package/bin/acc-update-worker.mjs +8 -0
  8. package/bin/acc.mjs +5 -39
  9. package/bin/entrypoints/acc-bootstrap.mjs +47 -0
  10. package/bin/entrypoints/acc-claude-channel.mjs +149 -0
  11. package/bin/entrypoints/acc-hook.mjs +58 -0
  12. package/bin/entrypoints/acc-mcp.mjs +70 -0
  13. package/bin/entrypoints/acc.mjs +51 -0
  14. package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
  15. package/bin/entrypoints/hook-output.mjs +73 -0
  16. package/docs/ADAPTER_AUTHORING.md +41 -7
  17. package/docs/ARCHITECTURE.md +62 -11
  18. package/docs/CAPABILITIES.md +40 -36
  19. package/docs/CLI.md +143 -25
  20. package/docs/CONCEPTS.md +13 -6
  21. package/docs/CONFIGURATION.md +37 -22
  22. package/docs/DESIGN_DECISIONS.md +16 -11
  23. package/docs/GETTING_STARTED.md +37 -7
  24. package/docs/GLOSSARY.md +9 -3
  25. package/docs/HOW_IT_WORKS.md +32 -11
  26. package/docs/MCP.md +91 -24
  27. package/docs/PROTOCOL.md +69 -12
  28. package/docs/RELEASING.md +28 -10
  29. package/docs/SECURITY_MODEL.md +45 -13
  30. package/docs/TROUBLESHOOTING.md +76 -18
  31. package/docs/UPGRADING.md +119 -0
  32. package/docs/WHY_ACC.md +5 -4
  33. package/docs/index.md +8 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +106 -5
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
  41. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
  51. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
  52. package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
  53. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +106 -5
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
  57. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
  58. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
  59. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
  60. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
  61. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +106 -5
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
  66. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  67. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +106 -5
  68. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
  69. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +106 -5
  71. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
  73. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
  81. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
  82. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
  83. package/node_modules/@agents-can-communicate/cli/package.json +4 -2
  84. package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
  85. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
  86. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
  87. package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
  88. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +22 -4
  89. package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
  96. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
  97. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
  98. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
  99. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
  100. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
  101. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
  102. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
  103. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
  104. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
  105. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
  106. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
  107. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
  109. package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
  110. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +9 -25
  111. package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
  112. package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
  113. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
  114. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
  115. package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
  116. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
  117. package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
  118. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
  119. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
  120. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
  121. package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
  122. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
  124. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
  125. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
  127. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
  128. package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
  129. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
  130. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
  131. package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
  132. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  133. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
  134. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
  135. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  137. package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
  138. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
  139. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
  140. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
  141. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
  143. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
  144. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
  145. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
  146. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  147. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
  148. package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
  149. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
  150. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
  151. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  152. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
  153. package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
  154. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
  155. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
  156. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
  157. package/package.json +2 -1
@@ -21,6 +21,9 @@ const data = (message, details) => {
21
21
  };
22
22
 
23
23
  export function assertMessageSemantics(message) {
24
+ if (message.decisionChange !== undefined && message.kind !== "decision") {
25
+ data("only a decision can replace or withdraw decisions", { kind: message.kind });
26
+ }
24
27
  const room = message.toParticipantIds.length === 0;
25
28
  if (room && !["note", "decision", "handoff"].includes(message.kind)) {
26
29
  data(`a room message cannot have kind ${message.kind}`, { kind: message.kind });
@@ -0,0 +1,15 @@
1
+ import { id, invalid, listOf, oneOf, plainObject } from "./fields.mjs";
2
+
3
+ export function assertDecisionChange(value, field = "decisionChange") {
4
+ plainObject(value, field);
5
+ for (const key of Object.keys(value)) {
6
+ if (!["action", "messageIds"].includes(key)) invalid(`${field}.${key}`, "is not a known field", value[key]);
7
+ }
8
+ oneOf("replace", "withdraw")(value.action, `${field}.action`);
9
+ listOf(id)(value.messageIds, `${field}.messageIds`);
10
+ if (value.messageIds.length < 1 || value.messageIds.length > 16
11
+ || new Set(value.messageIds).size !== value.messageIds.length) {
12
+ invalid(`${field}.messageIds`, "requires 1 to 16 unique decision message ids", value.messageIds);
13
+ }
14
+ return value;
15
+ }
@@ -2,7 +2,8 @@
2
2
  export { AccError, EXIT, isAccError } from "./errors.mjs";
3
3
  export { assertPortableId, createId } from "./ids.mjs";
4
4
  export { ENVELOPE_VERSION, failure, ok } from "./envelopes.mjs";
5
- export { RECORD_KINDS, SCHEMA_VERSION, validateRecord } from "./schema.mjs";
5
+ export { CLAIM_ENFORCEMENTS, CLAIM_MODES, HANDOFF_STATUSES, INTENT_MODES,
6
+ INTENT_STATES, RECORD_KINDS, SCHEMA_VERSION, validateRecord } from "./schema.mjs";
6
7
  export { GENERIC_MESSAGE_KINDS, MESSAGE_KINDS, OBLIGATIONS, VALID_OBLIGATIONS,
7
8
  assertMessageSemantics }
8
9
  from "./conversations.mjs";
@@ -10,3 +11,4 @@ export { CONFIG_FILENAME, CONFIG_SCHEMA_VERSION, RUNTIME_KEYS, defaultProjectCon
10
11
  validateProjectConfig } from "./config.mjs";
11
12
  export { RECEIPT_STATES, advanceReceipt } from "./states.mjs";
12
13
  export { assertMatchableResource, normaliseResource } from "./resources.mjs";
14
+ export { assertDecisionChange } from "./decision-changes.mjs";
@@ -1,10 +1,19 @@
1
1
  import { assertMessageSemantics, MESSAGE_KINDS, OBLIGATIONS } from "./conversations.mjs";
2
+ import { assertDecisionChange } from "./decision-changes.mjs";
2
3
  import { AccError, EXIT } from "./errors.mjs";
3
4
  import { id, invalid, listOf, nullable, oneOf, optional, plainObject, positiveInteger,
4
5
  resourceUri, sequence, text, timestamp } from "./fields.mjs";
5
6
 
6
7
  export const SCHEMA_VERSION = 3;
7
8
 
9
+ // Shared with CLI help so advertised choices are the ones durable records accept.
10
+ export const INTENT_MODES = Object.freeze(["observe", "explore", "edit", "review",
11
+ "coordinate", "wait"]);
12
+ export const INTENT_STATES = Object.freeze(["active", "blocked", "waiting", "done"]);
13
+ export const CLAIM_MODES = Object.freeze(["shared", "exclusive"]);
14
+ export const CLAIM_ENFORCEMENTS = Object.freeze(["advisory", "guarded"]);
15
+ export const HANDOFF_STATUSES = Object.freeze(["complete", "partial", "blocked"]);
16
+
8
17
  const line = text();
9
18
  const prose = text({ max: 4000, multiline: true });
10
19
  const summary = text({ max: 280 });
@@ -47,7 +56,7 @@ const artifactRef = (value, field) => {
47
56
  };
48
57
 
49
58
  const handoffPayload = (value, field) => closedObject(value, field, {
50
- status: oneOf("complete", "partial", "blocked"),
59
+ status: oneOf(...HANDOFF_STATUSES),
51
60
  completed: listOf(line),
52
61
  remaining: listOf(line),
53
62
  blockers: listOf(line),
@@ -90,19 +99,19 @@ const DURABLE_RECORDS = Object.freeze({
90
99
  heartbeatCadenceMs: positiveInteger, startedAt: timestamp, heartbeatAt: timestamp },
91
100
 
92
101
  intent: { sessionId: id, workspaceId: id, summary,
93
- mode: oneOf("observe", "explore", "edit", "review", "coordinate", "wait"),
102
+ mode: oneOf(...INTENT_MODES),
94
103
  resourceHints: listOf(resourceUri),
95
- state: oneOf("active", "blocked", "waiting", "done"), updatedAt: timestamp },
104
+ state: oneOf(...INTENT_STATES), updatedAt: timestamp },
96
105
 
97
106
  claim: { claimId: id, workspaceId: id, ownerSessionId: id, resource: resourceUri,
98
- mode: oneOf("shared", "exclusive"), enforcement: oneOf("advisory", "guarded"),
107
+ mode: oneOf(...CLAIM_MODES), enforcement: oneOf(...CLAIM_ENFORCEMENTS),
99
108
  reason: line, acquiredAt: timestamp, expiresAt: timestamp, generation: id },
100
109
 
101
110
  message: { messageId: id, threadId: id, clientMessageId: id, workspaceId: id,
102
111
  fromParticipantId: id, fromSessionId: id, toParticipantIds: listOf(id),
103
112
  kind: oneOf(...MESSAGE_KINDS), obligation: oneOf(...OBLIGATIONS),
104
113
  subject: line, body: prose, inReplyTo: nullable(id), artifacts: listOf(artifactRef),
105
- handoff: nullable(handoffPayload), sentAt: timestamp },
114
+ handoff: nullable(handoffPayload), decisionChange: optional(assertDecisionChange), sentAt: timestamp },
106
115
 
107
116
  receipt: { messageId: id, workspaceId: id, recipientParticipantId: id,
108
117
  state: receiptState, updatedAt: timestamp },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
6
6
 
7
+ import { assertPublicationDeadline } from "./deadline.mjs";
7
8
  import { assertManagedDirectory, ensureManagedDirectory } from "./safe-directory.mjs";
8
9
  import { readRegularNoFollow } from "./safe-file.mjs";
9
10
 
@@ -62,7 +63,8 @@ async function replaceHandleBytes(handle, bytes) {
62
63
  *
63
64
  * @returns {Promise<"published" | "already_published">}
64
65
  */
65
- export async function publishAtomic(destination, bytes, { root, tmpDir, replace = false }) {
66
+ export async function publishAtomic(destination, bytes, { root, tmpDir, replace = false, deadlineAt }) {
67
+ assertPublicationDeadline(deadlineAt);
66
68
  const destinationDir = path.dirname(destination);
67
69
  await Promise.all([
68
70
  ensureManagedDirectory(root, tmpDir),
@@ -87,12 +89,14 @@ export async function publishAtomic(destination, bytes, { root, tmpDir, replace
87
89
  if (replace) {
88
90
  await handle.close();
89
91
  handle = null;
92
+ assertPublicationDeadline(deadlineAt);
90
93
  await rename(temporary, destination);
91
94
  await syncDirectory(destinationDir);
92
95
  return "published";
93
96
  }
94
97
 
95
98
  try {
99
+ assertPublicationDeadline(deadlineAt);
96
100
  await link(temporary, destination);
97
101
  stageAcceptedBytes = true;
98
102
  await syncDirectory(destinationDir);
@@ -0,0 +1,7 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+
3
+ export function assertPublicationDeadline(deadlineAt) {
4
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
5
+ throw new AccError(EXIT.CONFLICT, "operation deadline expired before publication", {});
6
+ }
7
+ }
@@ -2,3 +2,5 @@
2
2
  export { openFilesystemStore, storePaths, ZERO_CURSOR } from "./store.mjs";
3
3
  export { diagnoseFilesystemStore, repairFilesystemStore } from "./recovery.mjs";
4
4
  export { readStoreIdentity, requireStoreIdentity, STORE_VERSION } from "./identity.mjs";
5
+ export { withWriterMutex } from "./writer-mutex.mjs";
6
+ export { readSessionRecord } from "./session-read.mjs";
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ import { assertPortableId, validateRecord } from "@agents-can-communicate/protocol";
3
+ import { readJsonIfPresent } from "./atomic-json.mjs";
4
+ import { requireStoreIdentity } from "./identity.mjs";
5
+ import { assertStateBinding, statePath } from "./record-id.mjs";
6
+ import { ephemeralIsDeleted, stateGenerationIsDeleted } from "./retention.mjs";
7
+ import { storePaths } from "./store.mjs";
8
+
9
+ /** Inspect ownership without opening, creating, recovering, or locking the store. */
10
+ export async function readSessionRecord({ root, workspaceId, sessionId }) {
11
+ assertPortableId(sessionId, "session id");
12
+ const paths = storePaths(root);
13
+ await requireStoreIdentity(paths, { workspaceId, create: false });
14
+ const file = statePath(paths, "session", sessionId);
15
+ const durable = await readJsonIfPresent(file, root);
16
+ if (durable !== null) {
17
+ const envelope = assertStateBinding(durable.value, "session", sessionId, file);
18
+ if (!await stateGenerationIsDeleted(paths, root, "session", sessionId, envelope.generation)) {
19
+ return validateRecord("session", envelope.record);
20
+ }
21
+ }
22
+ const ephemeral = await readJsonIfPresent(path.join(paths.ephemeral, "session", `${sessionId}.json`), root);
23
+ if (ephemeral === null || await ephemeralIsDeleted(paths, root, "session", sessionId)) return null;
24
+ return validateRecord("session", ephemeral.value);
25
+ }
@@ -7,6 +7,7 @@ import { AccError, EXIT, assertPortableId, validateRecord }
7
7
  import { encode, listDirectoryEntries, listJsonFiles, publishAtomic, readJsonIfPresent,
8
8
  retainFile } from "./atomic-json.mjs";
9
9
  import { initialiseActiveJournal } from "./active-journal.mjs";
10
+ import { assertPublicationDeadline } from "./deadline.mjs";
10
11
  import { requireStoreIdentity } from "./identity.mjs";
11
12
  import { journalEntry, readJournalCeiling, readOpenJournals, rollForward, writeJournalEntry }
12
13
  from "./journal.mjs";
@@ -32,13 +33,6 @@ const DIRECTORIES = ["state", "events", "journal", "locks", "ephemeral", "retain
32
33
 
33
34
  const pad = value => String(value).padStart(SEQUENCE_WIDTH, "0");
34
35
 
35
- function assertBeforePublication(deadlineAt) {
36
- if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
37
- throw new AccError(EXIT.CONFLICT,
38
- "transaction deadline expired before durable publication", {});
39
- }
40
- }
41
-
42
36
  export function storePaths(root) {
43
37
  return Object.freeze(Object.fromEntries([["root", root],
44
38
  ...DIRECTORIES.map(name => [name, path.join(root, name)])]));
@@ -77,7 +71,9 @@ async function nextSequence(paths, root) {
77
71
  return last === undefined ? 1 : Number(path.basename(last, ".json")) + 1;
78
72
  }
79
73
 
80
- export async function openFilesystemStore({ root, clock, ids, workspaceId, failAt }) {
74
+ export async function openFilesystemStore({ root, clock, ids, workspaceId, failAt,
75
+ deadlineAt: storeDeadline }) {
76
+ assertPublicationDeadline(storeDeadline);
81
77
  const paths = storePaths(root);
82
78
  // The caller owns the root path, so its ancestors are created here. Inside the
83
79
  // root, containment rules apply and each level is created individually so a
@@ -100,7 +96,7 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
100
96
  async function recoverOpenJournals() {
101
97
  const open = await readOpenJournals(paths, root);
102
98
  if (open.length === 0) return [];
103
- return withWriterMutex(paths, { root, tmpDir: paths.tmp, clock }, async () => {
99
+ return withWriterMutex(paths, { root, tmpDir: paths.tmp, clock, deadlineAt: storeDeadline }, async () => {
104
100
  const completed = [];
105
101
  for (const entry of await readOpenJournals(paths, root)) {
106
102
  completed.push(...await rollForward(paths, { root, tmpDir: paths.tmp, clock }, entry));
@@ -125,6 +121,8 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
125
121
  * Declaring nothing reads everything, which is what this always did.
126
122
  */
127
123
  async function transaction(callback, { kinds, deadlineAt } = {}) {
124
+ // A caller may tighten this invocation's budget, never extend it.
125
+ deadlineAt = Math.min(deadlineAt ?? Infinity, storeDeadline ?? Infinity);
128
126
  const wanted = kinds === undefined ? null : new Set(kinds);
129
127
  const declared = kind => {
130
128
  if (wanted !== null && !wanted.has(kind)) {
@@ -135,7 +133,7 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
135
133
  return kind;
136
134
  };
137
135
  return withWriterMutex(paths, { ...publishOptions, deadlineAt }, async () => {
138
- assertBeforePublication(deadlineAt);
136
+ assertPublicationDeadline(deadlineAt);
139
137
  // Reads are loaded once per transaction so get, list, and the generation
140
138
  // that put() compares against all describe the same instant.
141
139
  const loaded = await loadAllState(paths, root, wanted);
@@ -224,14 +222,13 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
224
222
  ];
225
223
  if (publications.length === 0) return result;
226
224
 
227
- // Cancellation is safe up to this point: nothing durable has decided the
228
- // transaction. Once the journal write starts, recovery must finish it and
229
- // the caller waits for that atomic outcome instead of reporting a false
230
- // timeout while publication continues in the background.
231
- assertBeforePublication(deadlineAt);
225
+ // Preparation is still cancellable. The active journal's atomic
226
+ // publication decides the write; roll-forward must then finish even if
227
+ // this invocation expires, so its publication options carry no deadline.
228
+ assertPublicationDeadline(deadlineAt);
232
229
  const entry = journalEntry(ids.next("transaction"), firstSequence, publications,
233
230
  clock.now());
234
- await writeJournalEntry(paths, publishOptions, entry);
231
+ await writeJournalEntry(paths, { ...publishOptions, deadlineAt }, entry);
235
232
  await failAt?.("after-journal");
236
233
  await rollForward(paths, publishOptions, entry);
237
234
  return result;
@@ -308,29 +305,35 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
308
305
  },
309
306
  async put(kind, id, record) {
310
307
  validateRecord(kind, record);
311
- return withWriterMutex(paths, publishOptions, async () => {
308
+ return withWriterMutex(paths, { ...publishOptions, deadlineAt: storeDeadline }, async () => {
312
309
  await publishAtomic(ephemeralPath(kind, id), encode(record),
313
- { root, tmpDir: paths.tmp, replace: true });
310
+ { root, tmpDir: paths.tmp, replace: true, deadlineAt: storeDeadline });
311
+ // Once record bytes are accepted, finish the same logical publication.
314
312
  await markEphemeral(paths, publishOptions, kind, id, "present");
315
313
  return record;
316
314
  });
317
315
  },
318
- async update(kind, id, updater) {
319
- return withWriterMutex(paths, publishOptions, async () => {
316
+ async update(kind, id, updater, { deadlineAt = storeDeadline } = {}) {
317
+ deadlineAt = Math.min(deadlineAt ?? Infinity, storeDeadline ?? Infinity);
318
+ return withWriterMutex(paths, { ...publishOptions, deadlineAt }, async () => {
320
319
  const next = await updater(await readEphemeral(kind, id));
321
320
  if (next === null) return null;
322
321
  validateRecord(kind, next);
322
+ assertPublicationDeadline(deadlineAt);
323
323
  await publishAtomic(ephemeralPath(kind, id), encode(next),
324
- { root, tmpDir: paths.tmp, replace: true });
324
+ { root, tmpDir: paths.tmp, replace: true, deadlineAt });
325
325
  await markEphemeral(paths, publishOptions, kind, id, "present");
326
326
  return next;
327
327
  });
328
328
  },
329
- async delete(kind, id) {
330
- return withWriterMutex(paths, publishOptions, async () => {
331
- if (await readEphemeral(kind, id) === null) return null;
329
+ async delete(kind, id, guard = () => true) {
330
+ return withWriterMutex(paths, { ...publishOptions, deadlineAt: storeDeadline }, async () => {
331
+ // Decide and delete under the same lock; a caller may be retiring an
332
+ // old generation while its replacement is waiting to write this id.
333
+ const current = await readEphemeral(kind, id);
334
+ if (current === null || !await guard(current)) return null;
332
335
  await retainFile(ephemeralPath(kind, id), { root });
333
- await markEphemeral(paths, publishOptions, kind, id, "deleted");
336
+ await markEphemeral(paths, { ...publishOptions, deadlineAt: storeDeadline }, kind, id, "deleted");
334
337
  return null;
335
338
  });
336
339
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Local-first coordination for independently opened AI agent sessions.",
6
6
  "keywords": [
@@ -60,6 +60,7 @@
60
60
  "docs/RELEASING.md",
61
61
  "docs/SECURITY_MODEL.md",
62
62
  "docs/TROUBLESHOOTING.md",
63
+ "docs/UPGRADING.md",
63
64
  "docs/WHY_ACC.md",
64
65
  "docs/index.md",
65
66
  "SECURITY.md",