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
@@ -1,126 +1,51 @@
1
- import { listSessionBindings } from "@agents-can-communicate/adapter-sdk";
2
1
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
3
2
 
4
- /**
5
- * Work out which session is running this command.
6
- *
7
- * Every mutating command needs a session id and the generation that proves the
8
- * caller is that session. Until this existed, both had to be passed on the
9
- * command line, and the shipped skills told agents to pass
10
- * `--session "$ACC_SESSION" --generation "$ACC_GENERATION"` - two variables that
11
- * nothing in the system has ever set. The generation is not printed by `acc
12
- * status` either, deliberately: it is the proof of ownership, not public
13
- * information. So the documented workflow could not be carried out by any agent
14
- * on any client, and the only way through was to read ACC's own files by hand,
15
- * which the same skill forbids in as many words.
16
- *
17
- * The hook runtime already knows both. It writes them into a binding at
18
- * SessionStart, keyed by the harness's own session id. What was missing was a
19
- * way for a shell inside that session to find its own binding. Each step below
20
- * answers that from something the caller demonstrably has.
21
- */
3
+ // Inbox retrieves bodies and advances receipts, so it needs the same owner
4
+ // proof as a mutation. Public observations never need somebody else's token.
22
5
  const NEEDS_OWNER = Object.freeze(new Set(["work", "claim", "release", "message",
23
- "inbox", "reply", "request", "ack", "workstream", "task", "finish", "decide"]));
24
-
25
- /**
26
- * Reads that are answers about *you*.
27
- *
28
- * Attention is computed per session: what is addressed to you, what has become
29
- * unblocked for you. Run by hand with nothing identifying the caller, both of
30
- * these answered for nobody and returned an empty list - so an agent told at the
31
- * top of its turn that work was waiting could ask `acc status` and be told there
32
- * was none. They resolve softly: a shell with no session attached still gets the
33
- * roster and the events, which is what it came for.
34
- */
6
+ "inbox", "reply", "request", "ack", "finish"]));
35
7
  const WANTS_OWNER = Object.freeze(new Set(["sync", "status"]));
36
8
 
37
9
  export const needsOwner = command => NEEDS_OWNER.has(command);
38
10
 
11
+ const unresolved = command => new AccError(EXIT.USAGE,
12
+ `${command} could not tell which session is running it. Pass both --session and `
13
+ + "--generation from this turn's ACC hook header or your own acc attach, or use this "
14
+ + "session's ACC MCP tools. Do not copy a peer's session from acc status. Native "
15
+ + "session IDs and a shared checkout do not establish CLI ownership.",
16
+ { command, reasonCode: "caller_identity_unresolved" });
17
+
39
18
  /**
40
- * Bindings whose session is still open, carrying the session's own checkout.
19
+ * Accept caller-supplied credentials; never discover credentials from peers.
41
20
  *
42
- * A binding outlives a crash, so one that names a closed or vanished session is
43
- * a leftover. Acting as a closed session would fail deeper in the service with a
44
- * conflict; dropping it here means the *next* candidate can be recognised
45
- * instead.
21
+ * A sole binding, a matching checkout, and a public session selector all let an
22
+ * unbound client adopt another session. Native environment values do too:
23
+ * nested clients inherit their parent's IDs. Bindings identify hook payloads,
24
+ * not arbitrary shell callers. ACC_SESSION/ACC_GENERATION remain an explicit
25
+ * operator-supplied pair, not an assertion that environment provenance is known.
46
26
  */
47
- async function liveCandidates({ context }) {
48
- const bindings = await listSessionBindings({ runtimeDir: context.paths.root });
49
- if (bindings.length === 0) return [];
50
- const status = await context.service.collectStatus({});
51
- const live = new Map(status.participants
52
- .filter(participant => participant.presence !== "offline")
53
- .map(participant => [participant.sessionId, participant]));
54
- return bindings
55
- .filter(binding => live.has(binding.accSessionId))
56
- .map(binding => ({ ...binding, session: live.get(binding.accSessionId) }));
57
- }
58
-
59
- const describe = candidates => candidates
60
- .map(candidate => `${candidate.session.participantId} (${candidate.session.harness})`)
61
- .join(", ");
62
-
63
27
  export async function resolveOwner({ command, options, context, env = {} }) {
64
28
  const soft = WANTS_OWNER.has(command);
65
29
  if (!soft && !needsOwner(command)) return options;
66
- if (options.session !== undefined && options.generation !== undefined) return options;
67
- if (soft && options.participant !== undefined) return options;
68
-
69
- const candidates = await liveCandidates({ context });
70
- const take = candidate => ({ ...options, session: candidate.accSessionId,
71
- generation: candidate.generation,
72
- // `status` filters by participant rather than by session, so it is told who
73
- // the caller is in the terms it works in.
74
- participant: options.participant ?? candidate.session.participantId });
75
-
76
- // Named a session but not the generation: the id is in `acc status --json`,
77
- // so this is what an agent reaches for first, and refusing it would teach the
78
- // wrong lesson about which half is secret.
79
- if (options.session !== undefined) {
80
- // The reads never needed a generation, and a session opened by `acc attach`
81
- // has no binding to find one in. Answering for the session it was given is
82
- // both what it was asked and what it did before this resolver existed.
30
+ const explicit = options.session !== undefined && options.generation !== undefined;
31
+ const configured = typeof env.ACC_SESSION === "string" && typeof env.ACC_GENERATION === "string";
32
+ if (!explicit && !configured) {
83
33
  if (soft) return options;
84
- const named = candidates.find(candidate => candidate.accSessionId === options.session);
85
- if (named !== undefined) return take(named);
86
- throw new AccError(EXIT.USAGE,
87
- `no live session ${options.session} in this workspace`,
88
- { command, session: options.session });
34
+ throw unresolved(command);
89
35
  }
90
36
 
91
- // The documented pair, honoured for anyone who has wired it up themselves.
92
- if (typeof env.ACC_SESSION === "string" && typeof env.ACC_GENERATION === "string") {
93
- return { ...options, session: env.ACC_SESSION, generation: env.ACC_GENERATION };
37
+ const owner = explicit ? options : { session: env.ACC_SESSION, generation: env.ACC_GENERATION };
38
+ if (options.session !== undefined && options.session !== owner.session) {
39
+ // A public observation scope is allowed; it does not request credentials.
40
+ if (soft) return options;
41
+ throw unresolved(command);
94
42
  }
95
-
96
- // The client's own session id, under whatever name it exports it. Claude Code
97
- // sets CLAUDE_CODE_SESSION_ID, measured equal to the `session_id` its hooks
98
- // receive; matching on the value rather than the variable name means a client
99
- // that names it something else works without ACC knowing the name.
100
- const exported = new Set(Object.values(env).filter(value => typeof value === "string"));
101
- const byEnvironment = candidates
102
- .filter(candidate => exported.has(candidate.harnessSessionId));
103
- if (byEnvironment.length === 1) return take(byEnvironment[0]);
104
-
105
- // Two agents in one workspace are the normal case, and each is usually in its
106
- // own checkout - that is what the worktree story is. Same repository, so the
107
- // same workspace, and the checkout tells them apart.
108
- const here = context.descriptor.git?.worktreeRoot;
109
- const byCheckout = here === undefined
110
- ? []
111
- : candidates.filter(candidate => candidate.session.checkoutRoot === here);
112
- if (byCheckout.length === 1) return take(byCheckout[0]);
113
-
114
- if (candidates.length === 1) return take(candidates[0]);
115
-
116
- // Guessing between two live sessions would let one agent act as another, which
117
- // is exactly what the generation exists to prevent. So this refuses, and says
118
- // what it found.
119
- if (soft) return options;
120
- throw new AccError(EXIT.USAGE, candidates.length === 0
121
- ? `${command} could not tell which session is running it: no live session is `
122
- + "attached here. An adapter attaches one at SessionStart; `acc status` shows who is."
123
- : `${command} could not tell which of ${candidates.length} live sessions is running `
124
- + `it (${describe(candidates)}). Pass --session, which \`acc status --json\` lists.`,
125
- { command, candidates: candidates.map(candidate => candidate.accSessionId) });
43
+ // A supplied generation must reach the core unchanged so stale calls fail.
44
+ const resolved = { ...options, session: owner.session,
45
+ generation: options.generation ?? owner.generation };
46
+ if (command === "status" && options.participant === undefined) {
47
+ const { participants } = await context.service.collectStatus({});
48
+ resolved.participant = participants.find(item => item.sessionId === owner.session)?.participantId;
49
+ }
50
+ return resolved;
126
51
  }
@@ -7,6 +7,8 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
7
7
  import { platformPaths } from "./platform-paths.mjs";
8
8
  import { checkingIsOff, fetchLatest, isNewer, writeCachedCheck } from "./update-check.mjs";
9
9
 
10
+ import { runManagedUpdate, validateUpdateOptions } from "./managed-runtime/command.mjs";
11
+
10
12
  const execFileAsync = promisify(execFile);
11
13
 
12
14
  /**
@@ -26,6 +28,14 @@ export const upgradeSteps = version => [
26
28
  const spell = ([command, argv]) => ` ${command} ${argv.join(" ")}`;
27
29
 
28
30
  export async function runUpdateCommand({ options, runtime }) {
31
+ if (options.check === true && options.apply === true) {
32
+ throw new AccError(EXIT.USAGE, "use either --check or --apply");
33
+ }
34
+ validateUpdateOptions(options);
35
+ if (runtime.managerRoot) return runManagedUpdate({ options, runtime });
36
+ if (options.auto !== undefined || options.pin !== undefined) {
37
+ throw new AccError(EXIT.USAGE, "run acc install before configuring automatic updates");
38
+ }
29
39
  const env = runtime.env ?? {};
30
40
  const { data: dataHome } = platformPaths({ platform: runtime.platform, env });
31
41
  const running = typeof runtime.version === "function"
@@ -61,26 +71,34 @@ export async function runUpdateCommand({ options, runtime }) {
61
71
  const data = { checked: true, running, latest, newer: true,
62
72
  steps: steps.map(([command, argv]) => [command, ...argv].join(" ")) };
63
73
 
64
- if (options.apply !== true) {
74
+ if (options.check === true) {
65
75
  return { data, text: [`acc ${latest} is available; you have ${running}`, "",
66
- ...steps.map(spell), "", "or run: acc update --apply"].join("\n") };
76
+ ...steps.map(spell), "", "or run: acc update"].join("\n") };
67
77
  }
68
78
 
69
79
  const spawn = runtime.spawn ?? ((command, argv) => execFileAsync(command, argv, { env }));
70
80
  const done = [];
81
+ let installation = { stdout: "", stderr: "" };
71
82
  for (const [command, argv] of steps) {
72
83
  try {
73
- await spawn(command, argv);
84
+ const output = await spawn(command, argv);
85
+ if (command === "acc") {
86
+ installation = { stdout: String(output?.stdout ?? "").trim(),
87
+ stderr: String(output?.stderr ?? "").trim() };
88
+ }
74
89
  done.push([command, ...argv].join(" "));
75
90
  } catch (error) {
76
91
  // Named rather than swallowed, and the rest of the commands are printed:
77
92
  // a global install refused for want of permission is the ordinary case,
78
93
  // and the person can finish it by hand from here.
94
+ const text = [`${command} failed: ${error.message}`, "", "finish it with:",
95
+ ...steps.slice(done.length).map(spell)].join("\n");
79
96
  return { data: { ...data, applied: done, failed: [command, ...argv].join(" ") },
80
- text: [`${command} failed: ${error.message}`, "", "finish it with:",
81
- ...steps.slice(done.length).map(spell)].join("\n"),
82
- error: undefined };
97
+ text, error: new AccError(EXIT.DATA, text) };
83
98
  }
84
99
  }
85
- return { data: { ...data, applied: done }, text: `updated to ${latest}` };
100
+ const activation = "Restart all running agent clients to load the updated ACC runtime and skills.";
101
+ return { data: { ...data, applied: done, installation, activation },
102
+ text: [`updated to ${latest}`, installation.stdout, installation.stderr, activation]
103
+ .filter(Boolean).join("\n\n") };
86
104
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,6 +2,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
2
 
3
3
  import { overlaps } from "./claims.mjs";
4
4
  import { classifySessionPresence } from "./sessions.mjs";
5
+ import { decisionView, isCurrentDecision } from "./decision-state.mjs";
5
6
 
6
7
  export const ATTENTION_PRIORITY = Object.freeze({
7
8
  reply_required: 1,
@@ -95,6 +96,8 @@ export function computeAttention(snapshot, { session, participantId, now, pidIsA
95
96
  if (typeof pidIsAlive !== "function") {
96
97
  throw new AccError(EXIT.USAGE, "computeAttention requires a pidIsAlive probe", {});
97
98
  }
99
+ snapshot = { ...snapshot, messages: (snapshot.messages ?? [])
100
+ .map(decisionView(snapshot.messages ?? [])).filter(isCurrentDecision) };
98
101
  return [
99
102
  ...obligationItems(snapshot, participantId),
100
103
  ...unavailableRecipients(snapshot, participantId, now, pidIsAlive),
@@ -33,17 +33,19 @@ const isLive = (claim, now) => Date.parse(claim.expiresAt) > Date.parse(now);
33
33
  export function createClaimService(ports, sessions) {
34
34
  const { store, clock, ids, pidIsAlive } = ports;
35
35
 
36
- async function requireOwner(input, action) {
37
- const existing = await sessions.locateSession(input.sessionId, input.workspaceId);
38
- if (existing === null || existing.record.state !== "open") {
36
+ async function requireOwner(input, action, tx) {
37
+ const current = tx === undefined
38
+ ? (await sessions.locateSession(input.sessionId, input.workspaceId))?.record
39
+ : tx.get("session", input.sessionId);
40
+ if (current == null || current.state !== "open") {
39
41
  throw new AccError(EXIT.CONFLICT, `cannot ${action} from a session that is not open`,
40
42
  { sessionId: input.sessionId });
41
43
  }
42
- if (existing.record.generation !== input.generation) {
44
+ if (current.generation !== input.generation) {
43
45
  throw new AccError(EXIT.CONFLICT, `cannot ${action} with a replaced session generation`,
44
46
  { sessionId: input.sessionId });
45
47
  }
46
- return existing.record;
48
+ return current;
47
49
  }
48
50
 
49
51
  function conflictWith(existing, owner, now, snapshotSessions) {
@@ -83,6 +85,7 @@ export function createClaimService(ports, sessions) {
83
85
 
84
86
  let record = null;
85
87
  await store.transaction(async tx => {
88
+ await requireOwner(input, "claim", tx);
86
89
  const live = tx.list("claim", claim => isLive(claim, now));
87
90
  // This session's own claim on this resource, live or lapsed. Looking only
88
91
  // among the live ones left the expired record behind and made a fresh one
@@ -117,7 +120,7 @@ export function createClaimService(ports, sessions) {
117
120
  actorSessionId: session.sessionId, type: mine === undefined ? "claim.acquired"
118
121
  : "claim.renewed", occurredAt: now,
119
122
  payload: { claimId, resource, mode: record.mode } });
120
- }, { kinds: ["claim"] });
123
+ }, { kinds: ["session", "claim"] });
121
124
  return record;
122
125
  }
123
126
 
@@ -128,6 +131,7 @@ export function createClaimService(ports, sessions) {
128
131
  .toISOString();
129
132
  let record = null;
130
133
  await store.transaction(async tx => {
134
+ await requireOwner(input, "renew", tx);
131
135
  const existing = tx.get("claim", input.claimId);
132
136
  if (existing === null || existing.ownerSessionId !== session.sessionId) {
133
137
  throw new AccError(EXIT.CONFLICT, "only the owning session generation may renew a claim",
@@ -138,7 +142,7 @@ export function createClaimService(ports, sessions) {
138
142
  tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
139
143
  workspaceId: existing.workspaceId, actorSessionId: session.sessionId,
140
144
  type: "claim.renewed", occurredAt: now, payload: { claimId: input.claimId } });
141
- }, { kinds: ["claim"] });
145
+ }, { kinds: ["session", "claim"] });
142
146
  return record;
143
147
  }
144
148
 
@@ -146,6 +150,7 @@ export function createClaimService(ports, sessions) {
146
150
  const session = await requireOwner(input, "release");
147
151
  const now = clock.now();
148
152
  await store.transaction(async tx => {
153
+ await requireOwner(input, "release", tx);
149
154
  const existing = tx.get("claim", input.claimId);
150
155
  if (existing === null) {
151
156
  throw new AccError(EXIT.CONFLICT, "the claim does not exist", { claimId: input.claimId });
@@ -167,7 +172,7 @@ export function createClaimService(ports, sessions) {
167
172
  type: owned ? "claim.released" : "claim.force_released", occurredAt: now,
168
173
  payload: { claimId: input.claimId, authority: authority ?? null,
169
174
  reason: reason ?? null, replacedGeneration: existing.generation } });
170
- }, { kinds: ["claim"] });
175
+ }, { kinds: ["session", "claim"] });
171
176
  }
172
177
 
173
178
  return {
@@ -1,11 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
-
4
3
  import { AccError, EXIT, SCHEMA_VERSION, validateRecord }
5
4
  from "@agents-can-communicate/protocol";
6
5
 
7
6
  import { projectReleasedClaim, reconstructFinishRetry } from "./finish-retries.mjs";
8
7
  import { ensureMaterialised } from "./materialisation.mjs";
8
+ import { nextTurnDelivery as selectNextTurnDelivery } from "./next-turn-delivery.mjs";
9
+ import { prepareDecisionChange } from "./decision-changes.mjs";
9
10
 
10
11
  export const receiptId = (messageId, participantId) => `receipt_${createHash("sha256")
11
12
  .update(JSON.stringify([messageId, participantId])).digest("base64url")}`;
@@ -19,6 +20,7 @@ const logicalContent = message => ({
19
20
  inReplyTo: message.inReplyTo,
20
21
  artifacts: message.artifacts,
21
22
  handoff: message.handoff,
23
+ decisionChange: message.decisionChange ?? null,
22
24
  });
23
25
 
24
26
  const normalizedContent = input => logicalContent({
@@ -30,6 +32,7 @@ const normalizedContent = input => logicalContent({
30
32
  inReplyTo: input.inReplyTo ?? null,
31
33
  artifacts: input.artifacts ?? [],
32
34
  handoff: input.handoff ?? null,
35
+ decisionChange: input.decisionChange,
33
36
  });
34
37
 
35
38
  function existingMessage(tx, session, input) {
@@ -127,8 +130,8 @@ export function recordMessageInTransaction({ tx, session, input: raw, now, messa
127
130
  // Resolved before anything else reads the recipients, so the stored message
128
131
  // names real participants and a retry compares against the same content. A
129
132
  // record that kept the client name would address nobody when it is read back.
130
- const input = { ...raw,
131
- toParticipantIds: resolveAddressees(tx, raw.toParticipantIds ?? [], session) };
133
+ const input = prepareDecisionChange(tx, { ...raw,
134
+ toParticipantIds: resolveAddressees(tx, raw.toParticipantIds ?? [], session) }, session);
132
135
  const existing = existingMessage(tx, session, input);
133
136
  if (existing !== undefined) {
134
137
  return { message: existing, recipientParticipantIds: [], created: false };
@@ -151,6 +154,7 @@ export function recordMessageInTransaction({ tx, session, input: raw, now, messa
151
154
  inReplyTo,
152
155
  artifacts: input.artifacts ?? [],
153
156
  handoff: input.handoff ?? null,
157
+ ...(input.decisionChange === undefined ? {} : { decisionChange: input.decisionChange }),
154
158
  sentAt: now,
155
159
  });
156
160
  const recipientParticipantIds = addressedRecipients(tx, message, session);
@@ -288,28 +292,8 @@ export function createConversationService(ports, sessions) {
288
292
  return (await nextTurnDelivery(input)).queuedMessages;
289
293
  }
290
294
 
291
- async function nextTurnDelivery(input = {}) {
292
- if (typeof input.participantId !== "string" || input.participantId === "") {
293
- return { queuedMessages: [], liveOfferedMessageIds: [], roomMessageIds: [] };
294
- }
295
- const snapshot = await store.snapshot(input.workspaceId ?? store.workspaceId,
296
- { kinds: ["message", "receipt"] });
297
- const receiptByMessage = new Map(snapshot.receipts
298
- .filter(item => item.recipientParticipantId === input.participantId)
299
- .map(item => [item.messageId, item]));
300
- const eligible = snapshot.messages.filter(message => receiptByMessage.has(message.messageId)
301
- && message.fromSessionId !== input.exceptSessionId)
302
- .sort((left, right) => left.sentAt.localeCompare(right.sentAt)
303
- || left.messageId.localeCompare(right.messageId));
304
- return {
305
- queuedMessages: eligible.filter(message =>
306
- receiptByMessage.get(message.messageId).state === "queued"),
307
- liveOfferedMessageIds: eligible.filter(message => message.toParticipantIds.length > 0
308
- && receiptByMessage.get(message.messageId).state === "offered")
309
- .map(message => message.messageId),
310
- roomMessageIds: eligible.filter(message => message.toParticipantIds.length === 0)
311
- .map(message => message.messageId),
312
- };
295
+ function nextTurnDelivery(input) {
296
+ return selectNextTurnDelivery(store, input);
313
297
  }
314
298
 
315
299
  return { sendMessage, finishSession, pendingMessages, nextTurnDelivery };
@@ -0,0 +1,36 @@
1
+ import { AccError, EXIT, assertDecisionChange } from "@agents-can-communicate/protocol";
2
+
3
+ // Resolve inherited recipients before retry comparison. Target messages and
4
+ // their recipient sets are immutable, so receipt advancement cannot alter a retry.
5
+ export function prepareDecisionChange(tx, input, session) {
6
+ if (input.decisionChange !== undefined) {
7
+ throw new AccError(EXIT.USAGE, "send a decision change using supersedes or withdraws");
8
+ }
9
+ if (input.supersedes === undefined && input.withdraws === undefined) return input;
10
+ if (input.kind !== "decision" || (input.supersedes !== undefined && input.withdraws !== undefined)) {
11
+ throw new AccError(EXIT.USAGE, "only decisions accept either supersedes or withdraws, never both");
12
+ }
13
+ const decisionChange = assertDecisionChange({
14
+ action: input.supersedes === undefined ? "withdraw" : "replace",
15
+ messageIds: input.supersedes ?? input.withdraws,
16
+ });
17
+ decisionChange.messageIds = [...decisionChange.messageIds].sort();
18
+ const targets = decisionChange.messageIds.map(messageId => {
19
+ const message = tx.get("message", messageId);
20
+ if (message?.kind !== "decision" || message.workspaceId !== session.workspaceId) {
21
+ throw new AccError(EXIT.DATA, "a decision change must reference existing decisions in this workspace",
22
+ { messageId });
23
+ }
24
+ return message;
25
+ });
26
+ if (new Set(input.toParticipantIds).size !== input.toParticipantIds.length) {
27
+ throw new AccError(EXIT.USAGE, "a participant may be addressed only once");
28
+ }
29
+ const targetIds = new Set(decisionChange.messageIds);
30
+ const inherited = [...targets.map(message => message.fromParticipantId),
31
+ ...tx.list("receipt", receipt => targetIds.has(receipt.messageId))
32
+ .map(receipt => receipt.recipientParticipantId)]
33
+ .filter(participantId => participantId !== session.participantId);
34
+ return { ...input, decisionChange,
35
+ toParticipantIds: [...new Set([...input.toParticipantIds, ...inherited])].sort() };
36
+ }
@@ -0,0 +1,42 @@
1
+ // Lifecycle is a read model of explicit peer assertions, never a timestamp
2
+ // winner or a consensus verdict. Iterative traversal avoids recursion limits.
3
+ export function decisionView(messages) {
4
+ const decisions = new Map(messages.filter(m => m.kind === "decision").map(m => [m.messageId, m]));
5
+ const neighbours = new Map([...decisions.keys()].map(id => [id, new Set()]));
6
+ const replaced = new Set();
7
+ for (const message of decisions.values()) {
8
+ for (const target of message.decisionChange?.messageIds ?? []) {
9
+ if (!decisions.has(target)) continue;
10
+ neighbours.get(target).add(message.messageId);
11
+ neighbours.get(message.messageId).add(target);
12
+ replaced.add(target);
13
+ }
14
+ }
15
+ const statuses = new Map();
16
+ for (const start of decisions.keys()) {
17
+ if (statuses.has(start)) continue;
18
+ const group = new Set([start]), pending = [start];
19
+ while (pending.length > 0) {
20
+ for (const id of neighbours.get(pending.pop())) {
21
+ if (!group.has(id)) { group.add(id); pending.push(id); }
22
+ }
23
+ }
24
+ const heads = [...group].filter(id => !replaced.has(id));
25
+ const hasDecision = heads.some(id => decisions.get(id).decisionChange?.action !== "withdraw");
26
+ const roots = [...group].filter(id => decisions.get(id).decisionChange === undefined);
27
+ const groupId = (roots.length > 0 ? roots : [...group]).sort()[0];
28
+ for (const id of group) {
29
+ const isHead = !replaced.has(id);
30
+ statuses.set(id, {
31
+ state: isHead ? (decisions.get(id).decisionChange?.action === "withdraw" ? "withdrawn" : "current")
32
+ : (hasDecision ? "superseded" : "withdrawn"),
33
+ isHead, conflicted: heads.length !== 1, headCount: heads.length,
34
+ currentMessageId: heads.length === 1 ? heads[0] : null, groupId,
35
+ });
36
+ }
37
+ }
38
+ return message => statuses.has(message.messageId)
39
+ ? { ...message, decisionStatus: statuses.get(message.messageId) } : message;
40
+ }
41
+
42
+ export const isCurrentDecision = message => message.decisionStatus?.isHead !== false;
@@ -81,18 +81,24 @@ export function createDeliveryBindingService(ports, sessions) {
81
81
  // delete() takes the same mutex, so this is the one atomic form: a stale
82
82
  // generation cannot retire a successor's endpoint, and an absent or
83
83
  // already-retired binding stays as it is.
84
+ // A caller that captured an endpoint may also scope retirement to that exact
85
+ // address. This protects a same-generation re-handshake that publishes while
86
+ // an older bounded cleanup call is still waiting to enter the writer lock.
84
87
  //
85
88
  // The retirement is recorded as its own fact rather than as an expired lease.
86
89
  // Expiry and retirement used to be the same edit, so a channel still renewing
87
90
  // its endpoint could extend a binding the session had already given up.
88
- async function clearDeliveryBinding({ sessionId, generation }) {
91
+ async function clearDeliveryBinding(input) {
92
+ const { sessionId, generation } = input;
93
+ const endpointScoped = Object.hasOwn(input, "opaqueEndpointRef");
89
94
  await store.ephemeral.update("deliveryBinding", sessionId, async current => {
90
95
  if (current === null || current === undefined) return null;
91
96
  if (current.generation !== generation) throw conflict(sessionId);
97
+ if (endpointScoped && current.opaqueEndpointRef !== input.opaqueEndpointRef) return null;
92
98
  if (current.retiredAt !== null && current.retiredAt !== undefined) return null;
93
99
  const now = clock.now();
94
100
  return { ...current, leaseUntil: now, retiredAt: now };
95
- });
101
+ }, { deadlineAt: input.deadlineAt });
96
102
  }
97
103
 
98
104
  /**
@@ -120,9 +126,10 @@ export function createDeliveryBindingService(ports, sessions) {
120
126
  });
121
127
  }
122
128
 
123
- async function listDeliveryBindings({ participantId, now }) {
129
+ async function listDeliveryBindings({ participantId, now, includeExpired = false }) {
124
130
  return (await currentBindings(now))
125
- .filter(item => item.participantId === participantId && item.reachable)
131
+ .filter(item => item.participantId === participantId
132
+ && (includeExpired || item.reachable))
126
133
  .map(item => item.binding);
127
134
  }
128
135
 
@@ -2,6 +2,8 @@ import { AccError, EXIT, SCHEMA_VERSION, advanceReceipt }
2
2
  from "@agents-can-communicate/protocol";
3
3
 
4
4
  import { receiptId, recordMessageInTransaction } from "./conversations.mjs";
5
+ import { messagePage, messageSummary } from "./message-pages.mjs";
6
+ import { decisionView, isCurrentDecision } from "./decision-state.mjs";
5
7
 
6
8
  const listable = (message, receipt) => receipt.state === "queued" || receipt.state === "offered"
7
9
  || (receipt.state === "retrieved" && message.obligation !== "none");
@@ -9,14 +11,19 @@ const listable = (message, receipt) => receipt.state === "queued" || receipt.sta
9
11
  export function createInboxService(ports, sessions) {
10
12
  const { store, clock, ids } = ports;
11
13
 
12
- async function requireOpen(input, action) {
13
- const located = await sessions.locateSession(input.sessionId, input.workspaceId);
14
- if (located === null || located.record.state !== "open"
15
- || located.record.generation !== input.generation) {
14
+ async function requireOpen(input, action, tx) {
15
+ // A solo inbox may have no durable session yet. Inside a transaction the
16
+ // same writer lock also protects this ephemeral read from replacement.
17
+ const current = tx === undefined
18
+ ? (await sessions.locateSession(input.sessionId, input.workspaceId))?.record
19
+ : tx.get("session", input.sessionId)
20
+ ?? await store.ephemeral.get("session", input.sessionId);
21
+ if (current == null || current.state !== "open"
22
+ || current.generation !== input.generation) {
16
23
  throw new AccError(EXIT.CONFLICT, `cannot ${action} from this session generation`,
17
24
  { sessionId: input.sessionId });
18
25
  }
19
- return located.record;
26
+ return current;
20
27
  }
21
28
 
22
29
  function requireOwnedReceipt(tx, session, messageId) {
@@ -46,19 +53,38 @@ export function createInboxService(ports, sessions) {
46
53
  return { message: owned.message, receipt };
47
54
  }
48
55
 
56
+ async function listInbox(input) {
57
+ const session = await requireOpen(input, "list the inbox");
58
+ return store.transaction(async tx => {
59
+ await requireOpen(input, "list the inbox", tx);
60
+ const receipts = new Map(tx.list("receipt", receipt =>
61
+ receipt.recipientParticipantId === session.participantId)
62
+ .map(receipt => [receipt.messageId, receipt]));
63
+ const all = tx.list("message"), view = decisionView(all);
64
+ const messages = all.filter(message => receipts.has(message.messageId)).map(view);
65
+ return messagePage(messages, input, {
66
+ include: message => isCurrentDecision(message) && listable(message, receipts.get(message.messageId)),
67
+ project: message => ({ message: messageSummary(message),
68
+ receipt: receipts.get(message.messageId) }),
69
+ });
70
+ }, { kinds: ["session", "message", "receipt"] });
71
+ }
72
+
49
73
  async function readInbox(input) {
50
74
  const session = await requireOpen(input, "read the inbox");
51
75
  const now = clock.now();
52
- return store.transaction(tx => {
53
- const messages = new Map(tx.list("message").map(item => [item.messageId, item]));
76
+ return store.transaction(async tx => {
77
+ await requireOpen(input, "read the inbox", tx);
78
+ const all = tx.list("message"), view = decisionView(all);
79
+ const messages = new Map(all.map(item => [item.messageId, view(item)]));
54
80
  let selected = tx.list("receipt", receipt =>
55
81
  receipt.recipientParticipantId === session.participantId)
56
82
  .filter(receipt => messages.has(receipt.messageId));
57
83
  if (input.messageId === undefined) {
58
- selected = selected.filter(receipt => listable(messages.get(receipt.messageId), receipt));
84
+ selected = selected.filter(receipt => listable(messages.get(receipt.messageId), receipt)
85
+ && isCurrentDecision(messages.get(receipt.messageId)));
59
86
  } else {
60
- selected = selected.filter(receipt => receipt.messageId === input.messageId
61
- && receipt.state !== "acknowledged");
87
+ selected = selected.filter(receipt => receipt.messageId === input.messageId);
62
88
  if (selected.length === 0) {
63
89
  throw new AccError(EXIT.CONFLICT,
64
90
  "that message is not recoverable by this participant",
@@ -70,17 +96,22 @@ export function createInboxService(ports, sessions) {
70
96
  const b = messages.get(right.messageId);
71
97
  return a.sentAt.localeCompare(b.sentAt) || a.messageId.localeCompare(b.messageId);
72
98
  });
73
- return selected.map(receipt => advanceOwned(tx, session, receipt.messageId,
74
- "retrieved", now));
75
- }, { kinds: ["message", "receipt"] });
99
+ // An exact read can inspect a resolved message. Inspection must neither
100
+ // move its receipt backward nor refresh its timestamp or emit an event.
101
+ return selected.map(receipt => receipt.state === "acknowledged"
102
+ ? { message: messages.get(receipt.messageId), receipt }
103
+ : { ...advanceOwned(tx, session, receipt.messageId, "retrieved", now),
104
+ message: messages.get(receipt.messageId) });
105
+ }, { kinds: ["session", "message", "receipt"] });
76
106
  }
77
107
 
78
108
  async function acknowledgeMessage(input) {
79
109
  const session = await requireOpen(input, "acknowledge a message");
80
110
  const now = clock.now();
81
- return store.transaction(tx => advanceOwned(tx, session, input.messageId,
82
- "acknowledged", now).receipt,
83
- { kinds: ["message", "receipt"] });
111
+ return store.transaction(async tx => {
112
+ await requireOpen(input, "acknowledge a message", tx);
113
+ return advanceOwned(tx, session, input.messageId, "acknowledged", now).receipt;
114
+ }, { kinds: ["session", "message", "receipt"] });
84
115
  }
85
116
 
86
117
  async function replyToMessage(input) {
@@ -114,5 +145,5 @@ export function createInboxService(ports, sessions) {
114
145
  }, { kinds: ["participant", "session", "message", "receipt"] });
115
146
  }
116
147
 
117
- return { readInbox, replyToMessage, acknowledgeMessage };
148
+ return { listInbox, readInbox, replyToMessage, acknowledgeMessage };
118
149
  }