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 +4 -1
- package/docs/rate-limiting.md +10 -0
- package/package.json +1 -1
- package/src/cli/mercury.ts +9 -0
- package/src/core/routes/chat.ts +43 -1
- package/src/core/routes/dashboard.ts +15 -5
- package/src/core/runtime.ts +10 -3
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.
|
|
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
|
|
package/docs/rate-limiting.md
CHANGED
|
@@ -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
package/src/cli/mercury.ts
CHANGED
|
@@ -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) {
|
package/src/core/routes/chat.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/src/core/runtime.ts
CHANGED
|
@@ -405,9 +405,16 @@ export class MercuryCoreRuntime {
|
|
|
405
405
|
: route.role === "admin"
|
|
406
406
|
? this.config.rateLimitDailyAdmin
|
|
407
407
|
: 0;
|
|
408
|
-
const
|
|
409
|
-
roleLimitRaw
|
|
410
|
-
(
|
|
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) {
|