mercury-agent 0.5.13 → 0.5.15

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.13",
3
+ "version": "0.5.15",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",
@@ -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)
@@ -1970,15 +1980,33 @@ export function createDashboardRoutes(ctx: DashboardContext) {
1970
1980
 
1971
1981
  const modelSelectorPanel = (() => {
1972
1982
  if (isMultiLeg) {
1983
+ const chainFromEnv = !!process.env.MERCURY_MODEL_CHAIN;
1984
+ const chainYaml = cfg.resolvedModelChain
1985
+ .map(
1986
+ (leg) =>
1987
+ ` - provider: ${leg.provider}\n model: ${leg.model}`,
1988
+ )
1989
+ .join("\n");
1990
+ const configSnippet = chainFromEnv
1991
+ ? `MERCURY_MODEL_CHAIN='${JSON.stringify(cfg.resolvedModelChain.map((l) => ({ provider: l.provider, model: l.model })))}'`
1992
+ : `model:\n chain:\n${chainYaml}`;
1993
+ const sourceLabel = chainFromEnv
1994
+ ? `<span class="mono">MERCURY_MODEL_CHAIN</span> env var in <span class="mono">.env</span>`
1995
+ : `<span class="mono">model.chain</span> in <span class="mono">mercury.yaml</span>`;
1996
+
1973
1997
  return `
1974
1998
  <div class="panel" style="margin-bottom:16px">
1975
1999
  <div class="panel-header" style="font-weight:600;font-size:13px">Active model</div>
1976
2000
  <div class="panel-body">
1977
2001
  ${renderModelBlock(cfg)}
1978
2002
  <p class="muted" style="font-size:12px;margin:8px 0 0">
1979
- Model chain is configured via <span class="mono">mercury.yaml</span> or
1980
- <span class="mono">MERCURY_MODEL_CHAIN</span> — edit those to change.
2003
+ Configured via ${sourceLabel}. Edit that file to change.
1981
2004
  </p>
2005
+ <details style="margin-top:8px">
2006
+ <summary class="muted" style="font-size:12px;cursor:pointer">Copy config</summary>
2007
+ <pre style="margin:6px 0 0;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius,4px);font-size:12px;overflow-x:auto;cursor:pointer;position:relative"
2008
+ onclick="navigator.clipboard.writeText(this.textContent.trim());var s=this.querySelector('.copy-hint');if(s){s.textContent='Copied!';setTimeout(function(){s.textContent='Click to copy'},1500)}">${escapeHtml(configSnippet)}<span class="copy-hint muted" style="position:absolute;top:4px;right:8px;font-size:11px">Click to copy</span></pre>
2009
+ </details>
1982
2010
  </div>
1983
2011
  </div>`;
1984
2012
  }
@@ -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) {