mercury-agent 0.5.14 → 0.5.16

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.
package/README.md CHANGED
@@ -403,6 +403,9 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
403
403
  | `MERCURY_CONTAINER_TIMEOUT_MS` | `300000` | Container timeout (5 min) |
404
404
  | `MERCURY_CONTAINER_RUNTIME` | `runc` | `runc` (default) or `runsc` ([gVisor](https://gvisor.dev)) |
405
405
  | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT` | `false` | Set `true` on Linux Docker Engine (see note below) |
406
+ | `MERCURY_AGENT_ID` | — | Unique ID for this assistant (required when running multiple assistants on the same Docker daemon) |
407
+
408
+ > **Multiple assistants on the same machine:** Mercury builds a derived Docker image (`mercury-agent-ext:<hash>`) from the base image plus your extensions. When a new image is built, older tags in the same repo are pruned. If two assistants share a Docker daemon without distinct `MERCURY_AGENT_ID` values, they share the same image repo and **one will silently prune the other's image**, causing container launch failures. Set `MERCURY_AGENT_ID` to a unique value per project (e.g. in `.env`). Cloud console deployments set this automatically.
406
409
 
407
410
  > **Linux Docker Engine:** Mercury uses [bubblewrap](https://github.com/containers/bubblewrap) for in-container sandboxing. On Linux Docker Engine (not Docker Desktop), bwrap cannot mount `/proc` without extra privileges. Either set `container_bwrap_docker_compat: true` in `mercury.yaml` (adds `--privileged` to `docker run`), or install [gVisor](https://gvisor.dev/docs/user_guide/install/) and set `MERCURY_CONTAINER_RUNTIME=runsc` to skip bwrap entirely.
408
411
 
@@ -440,7 +443,7 @@ runtime:
440
443
 
441
444
  `admin_ids` values are platform-specific identifiers — check the dashboard Conversations page to see the format (e.g. WhatsApp LID digits, Telegram numeric user ID).
442
445
 
443
- Auto-created spaces are seeded with `trigger.match=always`, `context.mode=context`, and the configured member permissions. Per-space rate limits are manageable from the dashboard.
446
+ Auto-created spaces are seeded with `trigger.match=always`, `context.mode=context`, and the configured member permissions. Changing `rate_limit_daily_member` in `mercury.yaml` propagates to all auto-created spaces immediately — no per-space update needed. Explicit per-space overrides (set via dashboard or API) still take precedence.
444
447
 
445
448
  ### Per-space Config
446
449
 
@@ -198,6 +198,16 @@ curl -X PUT http://localhost:8787/api/config \
198
198
  | Not set | No daily limit for this role (burst limiter still applies) |
199
199
  | Invalid (NaN) | Treated as "not set" |
200
200
 
201
+ ### Daily limit precedence
202
+
203
+ When resolving the effective daily limit for a role, Mercury uses this order:
204
+
205
+ 1. **Explicit per-space override** (set via dashboard, `mrctl`, or API) — always wins
206
+ 2. **Global config** (`rate_limit_daily_member` / `rate_limit_daily_admin` from `mercury.yaml` or env) — applies to all spaces without an explicit override
207
+ 3. **No limit** (0) — if the global config is 0 or unset, no daily cap is enforced
208
+
209
+ Auto-created DM spaces (from `dm_auto_space`) seed a `rate_limit.member` value on first contact, but this seed is treated as a deployment default, not an admin override. Changing `rate_limit_daily_member` in `mercury.yaml` takes effect immediately across all auto-created spaces — no DB migration or per-space update needed. If you explicitly set a per-space limit via the dashboard or API, that override takes precedence over the global config.
210
+
201
211
  ### How the two layers interact
202
212
 
203
213
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mercury-agent",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",
@@ -1940,6 +1940,10 @@ program
1940
1940
  .option("-s, --space <spaceId>", "Space to route the message to", "main")
1941
1941
  .option("-f, --file <paths...>", "Attach files to the message")
1942
1942
  .option("--caller <callerId>", "Caller ID", "cli:user")
1943
+ .option(
1944
+ "--platform <platform>",
1945
+ "Simulate platform for DM auto-space resolution (e.g. whatsapp)",
1946
+ )
1943
1947
  .option("--json", "Output raw JSON response")
1944
1948
  .action(
1945
1949
  async (
@@ -1949,6 +1953,7 @@ program
1949
1953
  space: string;
1950
1954
  file?: string[];
1951
1955
  caller: string;
1956
+ platform?: string;
1952
1957
  json?: boolean;
1953
1958
  },
1954
1959
  ) => {
@@ -1975,6 +1980,10 @@ program
1975
1980
  spaceId: options.space,
1976
1981
  };
1977
1982
 
1983
+ if (options.platform) {
1984
+ body.platform = options.platform;
1985
+ }
1986
+
1978
1987
  if (options.file && options.file.length > 0) {
1979
1988
  const files: Array<{ name: string; data: string }> = [];
1980
1989
  for (const filePath of options.file) {
@@ -5,6 +5,7 @@ import { Hono } from "hono";
5
5
  import { logger } from "../../logger.js";
6
6
  import { ensureSpaceWorkspace } from "../../storage/memory.js";
7
7
  import type { IngressMessage, MessageAttachment } from "../../types.js";
8
+ import { type AutoSpaceConfig, resolveConversation } from "../conversation.js";
8
9
  import { extToMime, mimeToMediaType } from "../media.js";
9
10
  import type { MercuryCoreRuntime } from "../runtime.js";
10
11
  import { isOverQuota } from "../storage-guard.js";
@@ -54,7 +55,7 @@ export function createChatRoute(core: MercuryCoreRuntime): Hono {
54
55
  ? body.callerId.trim()
55
56
  : "api:anonymous";
56
57
 
57
- const spaceId =
58
+ let spaceId =
58
59
  typeof body.spaceId === "string" && body.spaceId.trim()
59
60
  ? body.spaceId.trim()
60
61
  : "main";
@@ -62,6 +63,44 @@ export function createChatRoute(core: MercuryCoreRuntime): Hono {
62
63
  const authorName =
63
64
  typeof body.authorName === "string" ? body.authorName.trim() : undefined;
64
65
 
66
+ const platform =
67
+ typeof body.platform === "string" && body.platform.trim()
68
+ ? body.platform.trim()
69
+ : undefined;
70
+
71
+ if (platform && platform !== "api") {
72
+ const autoSpaceConfig: AutoSpaceConfig | undefined = core.config
73
+ .dmAutoSpaceEnabled
74
+ ? {
75
+ enabled: true,
76
+ adminIds: core.config.dmAutoSpaceAdminIds
77
+ .split(",")
78
+ .map((s) => s.trim())
79
+ .filter(Boolean),
80
+ defaultSystemPrompt: core.config.dmAutoSpaceDefaultSystemPrompt,
81
+ defaultMemberPermissions:
82
+ core.config.dmAutoSpaceDefaultMemberPermissions,
83
+ rateLimitDailyMember: core.config.rateLimitDailyMember,
84
+ }
85
+ : undefined;
86
+
87
+ const resolution = resolveConversation(
88
+ core.db,
89
+ platform,
90
+ callerId,
91
+ "dm",
92
+ undefined,
93
+ autoSpaceConfig,
94
+ authorName,
95
+ );
96
+
97
+ if (!resolution) {
98
+ return c.json({ error: "Conversation not linked to any space" }, 404);
99
+ }
100
+
101
+ spaceId = resolution.spaceId;
102
+ }
103
+
65
104
  if (!core.db.getSpace(spaceId)) {
66
105
  return c.json({ error: "Space not found" }, 404);
67
106
  }
@@ -126,6 +165,9 @@ export function createChatRoute(core: MercuryCoreRuntime): Hono {
126
165
  logger.info("API chat inbound", {
127
166
  callerId,
128
167
  spaceId,
168
+ ...(platform && platform !== "api"
169
+ ? { simulatedPlatform: platform }
170
+ : {}),
129
171
  preview: ingress.text.slice(0, 80),
130
172
  fileCount: attachments.length,
131
173
  });
@@ -531,16 +531,26 @@ export function createDashboardRoutes(ctx: DashboardContext) {
531
531
  const memberRaw = core.db.getSpaceConfig(spaceId, "rate_limit.member");
532
532
  const adminRaw = core.db.getSpaceConfig(spaceId, "rate_limit.admin");
533
533
 
534
+ const isSeeded = (key: string) =>
535
+ core.db.getSpaceConfig(spaceId, key) !== null &&
536
+ core.db.getSpaceConfigUpdatedBy(spaceId, key) === "dm-auto-space";
537
+
534
538
  const effectiveBurst = burstRaw ?? String(cfg.rateLimitPerUser);
535
539
  const effectiveMember =
536
- memberRaw ??
537
- (cfg.rateLimitDailyMember > 0 ? String(cfg.rateLimitDailyMember) : "0");
540
+ memberRaw && !isSeeded("rate_limit.member")
541
+ ? memberRaw
542
+ : cfg.rateLimitDailyMember > 0
543
+ ? String(cfg.rateLimitDailyMember)
544
+ : "0";
538
545
  const effectiveAdmin =
539
- adminRaw ??
540
- (cfg.rateLimitDailyAdmin > 0 ? String(cfg.rateLimitDailyAdmin) : "0");
546
+ adminRaw && !isSeeded("rate_limit.admin")
547
+ ? adminRaw
548
+ : cfg.rateLimitDailyAdmin > 0
549
+ ? String(cfg.rateLimitDailyAdmin)
550
+ : "0";
541
551
 
542
552
  const hasOverride = (key: string) =>
543
- core.db.getSpaceConfig(spaceId, key) !== null;
553
+ core.db.getSpaceConfig(spaceId, key) !== null && !isSeeded(key);
544
554
 
545
555
  const resetBtn = (key: string) =>
546
556
  hasOverride(key)
@@ -405,9 +405,16 @@ export class MercuryCoreRuntime {
405
405
  : route.role === "admin"
406
406
  ? this.config.rateLimitDailyAdmin
407
407
  : 0;
408
- const effectiveDailyRaw =
409
- roleLimitRaw ??
410
- (globalDailyLimit > 0 ? String(globalDailyLimit) : null);
408
+ const isSeededDefault =
409
+ roleLimitRaw !== null &&
410
+ this.db.getSpaceConfigUpdatedBy(message.spaceId, roleKey) ===
411
+ "dm-auto-space";
412
+ const effectiveDailyRaw = isSeededDefault
413
+ ? globalDailyLimit > 0
414
+ ? String(globalDailyLimit)
415
+ : null
416
+ : (roleLimitRaw ??
417
+ (globalDailyLimit > 0 ? String(globalDailyLimit) : null));
411
418
  if (effectiveDailyRaw !== null) {
412
419
  const roleLimit = Number.parseInt(effectiveDailyRaw, 10);
413
420
  if (!Number.isNaN(roleLimit) && roleLimit > 0) {