mixdog 1.2.6 → 1.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 (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +7 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -0,0 +1,446 @@
1
+ // Mixdog's built-in Google connectors (Gmail, Google Calendar, Google Drive):
2
+ // a small MCP server answered in-process at a reserved address
3
+ // (https://<service>.mixdog.invalid/mcp, RFC 2606: never resolvable, so no
4
+ // request or token can leave for it). It is an ordinary remote MCP server to
5
+ // the client, so the existing MCP OAuth sign-in applies unchanged: the
6
+ // protected-resource metadata names Google as the authorization server, the
7
+ // sign-in runs with Mixdog's pre-registered Desktop client (PKCE S256, state,
8
+ // loopback redirect, access_type=offline; mcp/oauth-client.mjs), the tokens
9
+ // live in the keychain, and each MCP request carries the access token, which
10
+ // this server spends on the Gmail / Calendar / Drive REST APIs.
11
+
12
+ import { accountInstructions } from '../shared/connector-accounts.mjs';
13
+
14
+ const PROTOCOL_VERSION = '2025-06-18';
15
+ const ORIGIN_SUFFIX = '.mixdog.invalid';
16
+ const GOOGLE_ISSUER = 'https://accounts.google.com';
17
+ const READ_CAP = 100_000;
18
+
19
+ const json = (status, body, headers = {}) =>
20
+ new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } });
21
+ const text = (value) => (typeof value === 'string' ? value.trim() : '');
22
+ const textResult = (value) => ({ content: [{ type: 'text', text: value }] });
23
+ const errorResult = (message) => ({ content: [{ type: 'text', text: message }], isError: true });
24
+
25
+ class GoogleUnauthorized extends Error {}
26
+ class ToolInputError extends Error {}
27
+
28
+ function requireText(args, key) {
29
+ const value = text(args?.[key]);
30
+ if (!value) throw new ToolInputError(`${key} is required`);
31
+ return value;
32
+ }
33
+
34
+ /** One Google REST call with the user's access token. */
35
+ async function google({ token, fetchImpl }, url, { method = 'GET', body, headers = {}, raw = false } = {}) {
36
+ const response = await fetchImpl(url, {
37
+ method,
38
+ headers: {
39
+ authorization: `Bearer ${token}`,
40
+ ...(body && !headers['content-type'] ? { 'content-type': 'application/json' } : {}),
41
+ ...headers,
42
+ },
43
+ body: typeof body === 'string' || body == null ? body : JSON.stringify(body),
44
+ });
45
+ if (response.status === 401) throw new GoogleUnauthorized();
46
+ if (!response.ok) {
47
+ const detail = await response.json().catch(() => null);
48
+ throw new Error(detail?.error?.message || `Google answered ${response.status}`);
49
+ }
50
+ if (raw) return response;
51
+ return response.status === 204 ? null : response.json();
52
+ }
53
+
54
+ // ── Gmail (gmail.send) ──────────────────────────────────────────────────────
55
+ const NO_HEADER_BREAKS = /[\r\n]/;
56
+ const encodedWord = (value) => `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
57
+
58
+ function mimeMessage({ to, cc, bcc, subject, body }) {
59
+ for (const [key, value] of Object.entries({ to, cc, bcc, subject })) {
60
+ if (value && NO_HEADER_BREAKS.test(value)) throw new ToolInputError(`${key} must be one line`);
61
+ }
62
+ const lines = [
63
+ `To: ${to}`,
64
+ cc ? `Cc: ${cc}` : '',
65
+ bcc ? `Bcc: ${bcc}` : '',
66
+ `Subject: ${encodedWord(subject)}`,
67
+ 'MIME-Version: 1.0',
68
+ 'Content-Type: text/plain; charset="UTF-8"',
69
+ 'Content-Transfer-Encoding: base64',
70
+ ].filter(Boolean);
71
+ return `${lines.join('\r\n')}\r\n\r\n${Buffer.from(body, 'utf8').toString('base64')}`;
72
+ }
73
+
74
+ const GMAIL_TOOLS = [
75
+ {
76
+ name: 'send_email',
77
+ description: 'Send a plain-text email from the signed-in Gmail account.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ to: { type: 'string', description: 'Recipients, comma-separated.' },
82
+ subject: { type: 'string' },
83
+ body: { type: 'string' },
84
+ cc: { type: 'string' },
85
+ bcc: { type: 'string' },
86
+ },
87
+ required: ['to', 'subject', 'body'],
88
+ },
89
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
90
+ async run(token, args) {
91
+ const raw = Buffer.from(
92
+ mimeMessage({
93
+ to: requireText(args, 'to'),
94
+ cc: text(args.cc),
95
+ bcc: text(args.bcc),
96
+ subject: text(args.subject),
97
+ body: String(args.body ?? ''),
98
+ })
99
+ ).toString('base64url');
100
+ const sent = await google(token, 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
101
+ method: 'POST',
102
+ body: { raw },
103
+ });
104
+ return `Sent (message ${sent.id}).`;
105
+ },
106
+ },
107
+ ];
108
+
109
+ // ── Google Calendar (calendar.events) ───────────────────────────────────────
110
+ const EVENTS = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
111
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
112
+
113
+ function when(value, timeZone) {
114
+ const at = text(value);
115
+ if (!at) return undefined;
116
+ if (DATE_ONLY.test(at)) return { date: at };
117
+ return { dateTime: at, ...(timeZone ? { timeZone } : {}) };
118
+ }
119
+
120
+ function eventBody(args) {
121
+ const timeZone = text(args.timeZone);
122
+ const body = {
123
+ summary: text(args.summary) || undefined,
124
+ description: typeof args.description === 'string' ? args.description : undefined,
125
+ location: text(args.location) || undefined,
126
+ start: when(args.start, timeZone),
127
+ end: when(args.end, timeZone),
128
+ attendees: Array.isArray(args.attendees)
129
+ ? args.attendees.map((email) => ({ email: text(email) })).filter((row) => row.email)
130
+ : undefined,
131
+ };
132
+ return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined));
133
+ }
134
+
135
+ const eventLine = (event) =>
136
+ JSON.stringify({
137
+ id: event.id,
138
+ summary: event.summary,
139
+ start: event.start?.dateTime || event.start?.date,
140
+ end: event.end?.dateTime || event.end?.date,
141
+ location: event.location,
142
+ attendees: event.attendees?.map((row) => row.email),
143
+ link: event.htmlLink,
144
+ });
145
+
146
+ const EVENT_FIELDS = {
147
+ summary: { type: 'string' },
148
+ start: { type: 'string', description: 'RFC 3339 date-time, or YYYY-MM-DD for all day.' },
149
+ end: { type: 'string', description: 'RFC 3339 date-time, or YYYY-MM-DD (exclusive) for all day.' },
150
+ timeZone: { type: 'string', description: 'IANA zone, e.g. Asia/Seoul.' },
151
+ description: { type: 'string' },
152
+ location: { type: 'string' },
153
+ attendees: { type: 'array', items: { type: 'string' }, description: 'Guest emails.' },
154
+ };
155
+
156
+ const CALENDAR_TOOLS = [
157
+ {
158
+ name: 'list_events',
159
+ description: 'List or search events on the primary calendar.',
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
163
+ timeMin: { type: 'string', description: 'RFC 3339; default now.' },
164
+ timeMax: { type: 'string', description: 'RFC 3339.' },
165
+ query: { type: 'string', description: 'Free-text search.' },
166
+ maxResults: { type: 'number', description: '1-100, default 25.' },
167
+ },
168
+ },
169
+ annotations: { readOnlyHint: true, openWorldHint: true },
170
+ async run(token, args) {
171
+ const params = new URLSearchParams({
172
+ singleEvents: 'true',
173
+ orderBy: 'startTime',
174
+ timeMin: text(args.timeMin) || new Date().toISOString(),
175
+ maxResults: String(Math.min(100, Math.max(1, Number(args.maxResults) || 25))),
176
+ });
177
+ if (text(args.timeMax)) params.set('timeMax', text(args.timeMax));
178
+ if (text(args.query)) params.set('q', text(args.query));
179
+ const list = await google(token, `${EVENTS}?${params}`);
180
+ const items = list.items || [];
181
+ return items.length ? items.map(eventLine).join('\n') : 'No events.';
182
+ },
183
+ },
184
+ {
185
+ name: 'create_event',
186
+ description: 'Create an event on the primary calendar.',
187
+ inputSchema: { type: 'object', properties: EVENT_FIELDS, required: ['summary', 'start', 'end'] },
188
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
189
+ async run(token, args) {
190
+ requireText(args, 'summary');
191
+ requireText(args, 'start');
192
+ requireText(args, 'end');
193
+ return eventLine(await google(token, EVENTS, { method: 'POST', body: eventBody(args) }));
194
+ },
195
+ },
196
+ {
197
+ name: 'update_event',
198
+ description: 'Change fields of an event on the primary calendar.',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: { eventId: { type: 'string' }, ...EVENT_FIELDS },
202
+ required: ['eventId'],
203
+ },
204
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
205
+ async run(token, args) {
206
+ const id = encodeURIComponent(requireText(args, 'eventId'));
207
+ return eventLine(await google(token, `${EVENTS}/${id}`, { method: 'PATCH', body: eventBody(args) }));
208
+ },
209
+ },
210
+ {
211
+ name: 'delete_event',
212
+ description: 'Delete an event from the primary calendar.',
213
+ inputSchema: { type: 'object', properties: { eventId: { type: 'string' } }, required: ['eventId'] },
214
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
215
+ async run(token, args) {
216
+ const id = encodeURIComponent(requireText(args, 'eventId'));
217
+ await google(token, `${EVENTS}/${id}`, { method: 'DELETE' });
218
+ return 'Deleted.';
219
+ },
220
+ },
221
+ ];
222
+
223
+ // ── Google Drive (drive.file: files this app created or the user opened with it)
224
+ const DRIVE = 'https://www.googleapis.com/drive/v3/files';
225
+ const FILE_FIELDS = 'id,name,mimeType,modifiedTime,webViewLink';
226
+ const EXPORT_AS = {
227
+ 'application/vnd.google-apps.document': 'text/plain',
228
+ 'application/vnd.google-apps.spreadsheet': 'text/csv',
229
+ 'application/vnd.google-apps.presentation': 'text/plain',
230
+ };
231
+ const TEXT_LIKE = /^text\/|^application\/(?:json|xml|javascript|x-yaml|csv)/;
232
+ const quoteQuery = (value) => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
233
+
234
+ const DRIVE_TOOLS = [
235
+ {
236
+ name: 'search_files',
237
+ description: 'List or search Drive files Mixdog can see (ones it created or you opened with it).',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ query: { type: 'string', description: 'Words in the file name.' },
242
+ pageSize: { type: 'number', description: '1-100, default 25.' },
243
+ },
244
+ },
245
+ annotations: { readOnlyHint: true, openWorldHint: true },
246
+ async run(token, args) {
247
+ const terms = ['trashed = false'];
248
+ if (text(args.query)) terms.push(`name contains '${quoteQuery(text(args.query))}'`);
249
+ const params = new URLSearchParams({
250
+ q: terms.join(' and '),
251
+ fields: `files(${FILE_FIELDS})`,
252
+ orderBy: 'modifiedTime desc',
253
+ pageSize: String(Math.min(100, Math.max(1, Number(args.pageSize) || 25))),
254
+ });
255
+ const list = await google(token, `${DRIVE}?${params}`);
256
+ const files = list.files || [];
257
+ return files.length ? files.map((file) => JSON.stringify(file)).join('\n') : 'No files.';
258
+ },
259
+ },
260
+ {
261
+ name: 'read_file',
262
+ description: 'Read a Drive file as text (Docs, Sheets as CSV, Slides, text files).',
263
+ inputSchema: { type: 'object', properties: { fileId: { type: 'string' } }, required: ['fileId'] },
264
+ annotations: { readOnlyHint: true, openWorldHint: true },
265
+ async run(token, args) {
266
+ const id = encodeURIComponent(requireText(args, 'fileId'));
267
+ const meta = await google(token, `${DRIVE}/${id}?fields=${FILE_FIELDS}`);
268
+ const exportAs = EXPORT_AS[meta.mimeType];
269
+ let url = '';
270
+ if (exportAs) url = `${DRIVE}/${id}/export?mimeType=${encodeURIComponent(exportAs)}`;
271
+ else if (TEXT_LIKE.test(meta.mimeType || '')) url = `${DRIVE}/${id}?alt=media`;
272
+ if (!url) return `${JSON.stringify(meta)}\n(Not a text file; open it at the link.)`;
273
+ const body = await (await google(token, url, { raw: true })).text();
274
+ const clipped = body.length > READ_CAP ? `${body.slice(0, READ_CAP)}\n[truncated]` : body;
275
+ return `${JSON.stringify(meta)}\n\n${clipped}`;
276
+ },
277
+ },
278
+ {
279
+ name: 'create_file',
280
+ description: 'Create a text file (or a Google Doc) in Drive.',
281
+ inputSchema: {
282
+ type: 'object',
283
+ properties: {
284
+ name: { type: 'string' },
285
+ content: { type: 'string' },
286
+ mimeType: { type: 'string', description: 'Default text/plain.' },
287
+ asGoogleDoc: { type: 'boolean', description: 'Convert to a Google Doc.' },
288
+ folderId: { type: 'string' },
289
+ },
290
+ required: ['name', 'content'],
291
+ },
292
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
293
+ async run(token, args) {
294
+ const metadata = {
295
+ name: requireText(args, 'name'),
296
+ ...(args.asGoogleDoc === true ? { mimeType: 'application/vnd.google-apps.document' } : {}),
297
+ ...(text(args.folderId) ? { parents: [text(args.folderId)] } : {}),
298
+ };
299
+ const boundary = `mixdog-${Date.now().toString(36)}`;
300
+ const body = [
301
+ `--${boundary}`,
302
+ 'Content-Type: application/json; charset=UTF-8',
303
+ '',
304
+ JSON.stringify(metadata),
305
+ `--${boundary}`,
306
+ `Content-Type: ${text(args.mimeType) || 'text/plain'}; charset=UTF-8`,
307
+ '',
308
+ String(args.content ?? ''),
309
+ `--${boundary}--`,
310
+ '',
311
+ ].join('\r\n');
312
+ const file = await google(
313
+ token,
314
+ `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=${FILE_FIELDS}`,
315
+ {
316
+ method: 'POST',
317
+ body,
318
+ headers: { 'content-type': `multipart/related; boundary=${boundary}` },
319
+ }
320
+ );
321
+ return JSON.stringify(file);
322
+ },
323
+ },
324
+ ];
325
+
326
+ const SERVICES = {
327
+ gmail: { name: 'Gmail', tools: GMAIL_TOOLS },
328
+ calendar: { name: 'Google Calendar', tools: CALENDAR_TOOLS },
329
+ drive: { name: 'Google Drive', tools: DRIVE_TOOLS },
330
+ };
331
+
332
+ /** The reserved MCP address of built-in Google service `service`. */
333
+ export function googleMcpUrl(service) {
334
+ return `https://${service}${ORIGIN_SUFFIX}/mcp`;
335
+ }
336
+
337
+ /** The built-in service a URL addresses, or null. */
338
+ export function googleMcpService(input) {
339
+ let url;
340
+ try {
341
+ url = new URL(String(input));
342
+ } catch {
343
+ return null;
344
+ }
345
+ if (url.protocol !== 'https:' || !url.hostname.endsWith(ORIGIN_SUFFIX)) return null;
346
+ return Object.hasOwn(SERVICES, url.hostname.slice(0, -ORIGIN_SUFFIX.length)) ? url : null;
347
+ }
348
+
349
+ const bearerOf = (headers) => /^Bearer\s+(\S+)$/i.exec(headers.get('authorization') || '')?.[1] || '';
350
+
351
+ /** `{ ok, email }`: whether Google still accepts `token` (an expired or
352
+ * revoked one is not), and the account it was issued for ('' when unnamed). */
353
+ async function tokenInfo({ token, fetchImpl }) {
354
+ const response = await fetchImpl(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`);
355
+ if (!response.ok) return { ok: false, email: '' };
356
+ const info = await response.json().catch(() => null);
357
+ return { ok: true, email: text(info?.email) };
358
+ }
359
+
360
+ async function rpc(service, token, message) {
361
+ const { id, method, params } = message || {};
362
+ const reply = (result) => ({ jsonrpc: '2.0', id, result });
363
+ if (method === 'initialize') {
364
+ const info = await tokenInfo(token);
365
+ if (!info.ok) throw new GoogleUnauthorized();
366
+ return reply({
367
+ protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
368
+ capabilities: { tools: { listChanged: false } },
369
+ serverInfo: { name: `mixdog-${service.key}`, version: '1.0.0' },
370
+ instructions: accountInstructions(service.name, info.email),
371
+ });
372
+ }
373
+ if (method === 'ping') return reply({});
374
+ if (method === 'tools/list') {
375
+ // Several accounts of one service are separate servers: each tool says whose it is.
376
+ const { email } = await tokenInfo(token).catch(() => ({ email: '' }));
377
+ return reply({
378
+ tools: service.tools.map(({ run, ...tool }) => ({
379
+ ...tool,
380
+ ...(email ? { description: `${tool.description} (${service.name} account: ${email})` } : {}),
381
+ })),
382
+ });
383
+ }
384
+ if (method === 'tools/call') {
385
+ const tool = service.tools.find((candidate) => candidate.name === params?.name);
386
+ if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
387
+ try {
388
+ return reply(textResult(await tool.run(token, params?.arguments || {})));
389
+ } catch (error) {
390
+ if (error instanceof GoogleUnauthorized) throw error;
391
+ return reply(errorResult(error?.message || String(error)));
392
+ }
393
+ }
394
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
395
+ }
396
+
397
+ /** Answers a request to a built-in Google service like a remote MCP server. */
398
+ async function answer(url, init, fetchImpl) {
399
+ const key = url.hostname.slice(0, -ORIGIN_SUFFIX.length);
400
+ const service = { key, ...SERVICES[key] };
401
+ const resource = googleMcpUrl(key);
402
+ const metadataUrl = `https://${url.hostname}/.well-known/oauth-protected-resource/mcp`;
403
+ if (url.pathname.startsWith('/.well-known/oauth-protected-resource')) {
404
+ return json(200, { resource, authorization_servers: [GOOGLE_ISSUER], bearer_methods_supported: ['header'] });
405
+ }
406
+ if (url.pathname !== '/mcp') return json(404, { error: 'not_found' });
407
+ const method = String(init?.method || 'GET').toUpperCase();
408
+ if (method !== 'POST') return new Response(null, { status: 405, headers: { allow: 'POST' } });
409
+ const unauthorized = () =>
410
+ json(401, { error: 'invalid_token' }, { 'www-authenticate': `Bearer resource_metadata="${metadataUrl}"` });
411
+ const bearer = bearerOf(new Headers(init?.headers));
412
+ if (!bearer) return unauthorized();
413
+ const token = { token: bearer, fetchImpl };
414
+ let message;
415
+ try {
416
+ message = JSON.parse(typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body));
417
+ } catch {
418
+ return json(400, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
419
+ }
420
+ const batch = Array.isArray(message) ? message : [message];
421
+ try {
422
+ const replies = (
423
+ await Promise.all(batch.map((one) => (one?.id === undefined ? null : rpc(service, token, one))))
424
+ ).filter(Boolean);
425
+ if (!replies.length) return new Response(null, { status: 202 });
426
+ return json(200, Array.isArray(message) ? replies : replies[0]);
427
+ } catch (error) {
428
+ if (error instanceof GoogleUnauthorized) return unauthorized();
429
+ throw error;
430
+ }
431
+ }
432
+
433
+ /** A `fetch` that answers the built-in Google services in-process and sends
434
+ * every other request (Google's sign-in and token endpoints) to `next`. */
435
+ export function withGoogleMcp(next = fetch) {
436
+ return (input, init) => {
437
+ const url = googleMcpService(input instanceof Request ? input.url : input);
438
+ return url
439
+ ? answer(
440
+ url,
441
+ input instanceof Request ? { method: input.method, headers: input.headers, body: init?.body } : init,
442
+ next
443
+ )
444
+ : next(input, init);
445
+ };
446
+ }
@@ -0,0 +1,69 @@
1
+ // Who may read and write which core-memory pool. Pools: common memory
2
+ // (project_id and agent_id NULL), a project's (project_id), and an agent's own
3
+ // (agent_id, project_id NULL).
4
+ //
5
+ // The caller arrives as `memory_caller` `{ kind, agentId }`, resolved by the
6
+ // session runtime from the calling session
7
+ // (session-runtime/services/memory-access.mjs); the model never supplies it. A
8
+ // call without one is the host acting for the user (desktop panels, TUI,
9
+ // admin routes).
10
+ // - common/project memory: every caller reads it; only a conversation with
11
+ // the user (Mixy's home, a Lead session, an agent's home thread) or the
12
+ // host changes it;
13
+ // - an agent's memory: that agent alone reads and writes it, from its home
14
+ // thread or its own unattended runs; Mixy and the host may also list and
15
+ // delete it. An unattended run (schedule, webhook, room member, delegated
16
+ // agent) therefore writes nowhere else, so external content cannot plant
17
+ // standing instructions in a pool another session reads.
18
+ // A deleted agent's pool is retained, unreferenced: its rows stay until the
19
+ // user deletes them, and an agent created later under the same id sees them.
20
+ import { agentHomeSessionId } from '../../agent/orchestrator/session/agent-home.mjs';
21
+
22
+ // 'host' names the absent caller explicitly; claiming it grants nothing a call
23
+ // without `memory_caller` would not.
24
+ const CALLER_KINDS = new Set(['host', 'human', 'agent', 'room-member', 'headless']);
25
+ const INTERACTIVE_KINDS = new Set(['host', 'human', 'agent']);
26
+
27
+ export const HOST_MEMORY_CALLER = Object.freeze({ kind: 'host', agentId: '', interactive: true });
28
+
29
+ /** `value` as a normalized agent id, or ''. */
30
+ export function validAgentId(value) {
31
+ const id = typeof value === 'string' ? value.trim() : '';
32
+ return id && agentHomeSessionId(id) ? id : '';
33
+ }
34
+
35
+ /** The caller `{ kind, agentId, interactive }` of a `memory_caller` value; an
36
+ * unrecognized one is an unattended run without an agent (fail closed). */
37
+ export function normalizeMemoryCaller(value) {
38
+ if (value == null || value.kind === 'host') return HOST_MEMORY_CALLER;
39
+ const kind = CALLER_KINDS.has(value?.kind) ? value.kind : 'headless';
40
+ const agentId = validAgentId(value?.agentId);
41
+ if (kind === 'agent' && !agentId) return { kind: 'headless', agentId: '', interactive: false };
42
+ return { kind, agentId, interactive: INTERACTIVE_KINDS.has(kind) };
43
+ }
44
+
45
+ const mayManageAgents = (caller) => caller.kind === 'host' || caller.kind === 'human';
46
+
47
+ export function corePoolLabel({ projectId = null, agentId = null } = {}) {
48
+ if (agentId) return `agent=${agentId}`;
49
+ return `project=${projectId ?? 'COMMON'}`;
50
+ }
51
+
52
+ /** Throws unless `caller` may read agent `agentId`'s pool (none: common/project). */
53
+ export function assertCoreRead(caller, agentId) {
54
+ if (!agentId || caller.agentId === agentId || mayManageAgents(caller)) return;
55
+ throw new Error(`the memory of agent "${agentId}" is readable only by that agent`);
56
+ }
57
+
58
+ /** Throws unless `caller` may `op` ('add' | 'edit' | 'delete') an entry of `pool`. */
59
+ export function assertCoreWrite(caller, op, pool = {}) {
60
+ const agentId = pool.agentId || null;
61
+ if (agentId) {
62
+ if (caller.agentId === agentId || (op === 'delete' && mayManageAgents(caller))) return;
63
+ throw new Error(`memory ${op} refused: the memory of agent "${agentId}" is written only by that agent`);
64
+ }
65
+ if (caller.interactive) return;
66
+ throw new Error(
67
+ `memory ${op} refused: ${pool.projectId ? `project memory "${pool.projectId}"` : 'common memory'} changes only in a conversation with the user; an unattended run writes only its own agent memory (agent_id "self")`
68
+ );
69
+ }
@@ -44,6 +44,7 @@ function normalizeCuratedEntry(row) {
44
44
  indexRevision: row?.indexRevision ?? row?.index_revision ?? null,
45
45
  summary,
46
46
  projectId: normalizeProjectId(row?.projectId ?? row?.project_id),
47
+ agentId: normalizeProjectId(row?.agentId ?? row?.agent_id),
47
48
  updatedAt: finiteNumber(row?.updatedAt ?? row?.updated_at),
48
49
  };
49
50
  }
@@ -115,7 +116,7 @@ export async function refreshCoreMemoryFile(db, dataDir) {
115
116
  const revision = reserveRevision(dataDir);
116
117
  const directory = await syncCoreMemoryIndexes(db);
117
118
  const curatedResult = await db.query(`
118
- SELECT id, summary, project_id, updated_at
119
+ SELECT id, summary, project_id, agent_id, updated_at
119
120
  FROM core_entries
120
121
  WHERE status IS NULL OR status = 'active'
121
122
  ORDER BY project_id NULLS FIRST, id ASC
@@ -129,16 +130,23 @@ export async function refreshCoreMemoryFile(db, dataDir) {
129
130
  );
130
131
  }
131
132
 
132
- export function readSessionCoreMemoryPayload(dataDir, cwd) {
133
+ /** Common, then the `cwd` project's, then agent `agentId`'s own entries (none
134
+ * without an agent); the caller's byte budget cuts from the end. */
135
+ export function readSessionCoreMemoryPayload(dataDir, cwd, agentId = '') {
133
136
  const file = readCoreMemoryFile(dataDir);
134
137
  if (!file) return null;
135
138
  const projectId = resolveProjectScope(cwd);
136
- const inScope = (entry) => entry.projectId === null || entry.projectId === projectId;
137
- const curated = file.curated.filter(inScope).sort((a, b) => {
138
- if (a.projectId === null && b.projectId !== null) return -1;
139
- if (a.projectId !== null && b.projectId === null) return 1;
140
- return a.id - b.id;
141
- });
139
+ const agent = normalizeProjectId(agentId);
140
+ const rank = (entry) => {
141
+ if (entry.agentId !== null) return agent !== null && entry.agentId === agent ? 2 : -1;
142
+ if (entry.projectId === null) return 0;
143
+ return entry.projectId === projectId ? 1 : -1;
144
+ };
145
+ const curated = file.curated
146
+ .map((entry) => ({ entry, rank: rank(entry) }))
147
+ .filter(({ rank: value }) => value >= 0)
148
+ .sort((a, b) => a.rank - b.rank || a.entry.id - b.entry.id)
149
+ .map(({ entry }) => entry);
142
150
  return {
143
151
  projectId,
144
152
  revision: file.revision,
@@ -1,7 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { corePoolLabel } from './core-memory-access.mjs';
2
3
 
3
4
  const PREFIX = 'memory.core.index.';
4
- const scopeKey = (scope) => JSON.stringify(scope ?? null);
5
+ // A project scope keys as before (`null` for common, `"slug"`); an agent's own
6
+ // pool as `{"agent":"id"}`.
7
+ const scopeKey = (scope, agentId = null) =>
8
+ agentId ? JSON.stringify({ agent: agentId }) : JSON.stringify(scope ?? null);
5
9
 
6
10
  // Stable database keys remain internal. This persisted directory is the
7
11
  // public, per-scope address space, including its stale-request fence.
@@ -10,7 +14,7 @@ export async function syncCoreMemoryIndexes(db) {
10
14
  await tx.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [PREFIX]);
11
15
  const records = (
12
16
  await tx.query(`
13
- SELECT id, project_id FROM core_entries
17
+ SELECT id, project_id, agent_id FROM core_entries
14
18
  WHERE status IS NULL OR status = 'active' ORDER BY id ASC
15
19
  `)
16
20
  ).rows;
@@ -24,7 +28,7 @@ export async function syncCoreMemoryIndexes(db) {
24
28
  const groups = new Map();
25
29
  for (const row of records) {
26
30
  if (!Number.isSafeInteger(Number(row.id)) || Number(row.id) <= 0) throw new Error('invalid internal memory key');
27
- const key = scopeKey(row.project_id);
31
+ const key = scopeKey(row.project_id, row.agent_id);
28
32
  if (!groups.has(key)) groups.set(key, []);
29
33
  groups.get(key).push(Number(row.id));
30
34
  }
@@ -55,7 +59,7 @@ export async function syncCoreMemoryIndexes(db) {
55
59
  }
56
60
 
57
61
  export function indexedCoreRecord(row, directory) {
58
- const state = directory.get(scopeKey(row.project_id ?? row.projectId));
62
+ const state = directory.get(scopeKey(row.project_id ?? row.projectId, row.agent_id ?? row.agentId));
59
63
  const index = state?.recordIds.indexOf(Number(row.id)) ?? -1;
60
64
  return {
61
65
  ...row,
@@ -65,23 +69,25 @@ export function indexedCoreRecord(row, directory) {
65
69
  };
66
70
  }
67
71
 
68
- export async function resolveCoreMemoryIndex(db, scope, index, revision) {
69
- if (scope === '*') throw new Error('memory writes require one explicit project scope');
72
+ /** `agentId` addresses that agent's own pool instead of the project `scope`. */
73
+ export async function resolveCoreMemoryIndex(db, scope, index, revision, agentId = null) {
74
+ if (scope === '*' && !agentId) throw new Error('memory writes require one explicit project scope');
70
75
  if (!Number.isSafeInteger(Number(index)) || Number(index) <= 0)
71
76
  throw new Error('memory index must be a positive integer');
72
77
  if (typeof revision !== 'string' || !revision)
73
78
  throw new Error('index_revision required; list memories before editing or deleting');
74
79
  const directory = await syncCoreMemoryIndexes(db);
75
- const state = directory.get(scopeKey(scope));
80
+ const state = directory.get(scopeKey(scope, agentId));
76
81
  if (!state || state.revision !== revision)
77
82
  throw new Error('memory indices changed; list memories again before editing or deleting');
78
83
  const recordId = state.recordIds[Number(index) - 1];
79
- if (!recordId) throw new Error(`no memory index=${index} in project=${scope ?? 'common'}`);
84
+ if (!recordId) throw new Error(`no memory index=${index} in ${corePoolLabel({ projectId: scope, agentId })}`);
80
85
  return recordId;
81
86
  }
82
87
 
83
88
  export async function publicCoreMemoryIdentity(db, row) {
84
89
  const indexed = indexedCoreRecord(row, await syncCoreMemoryIndexes(db));
85
90
  if (!indexed.id) throw new Error('memory changed before its index could be returned; list memories again');
86
- return `project=${row.project_id ?? 'COMMON'} id=${indexed.id} index_revision=${indexed.index_revision}`;
91
+ const pool = corePoolLabel({ projectId: row.project_id, agentId: row.agent_id });
92
+ return `${pool} id=${indexed.id} index_revision=${indexed.index_revision}`;
87
93
  }