mercury-agent 0.13.0 → 0.14.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.
@@ -96,8 +96,9 @@ Containers have a maximum runtime to prevent runaway processes.
96
96
  When a container exceeds the timeout:
97
97
  1. Container is killed via `docker kill`
98
98
  2. `ContainerError` thrown with `reason: "timeout"`
99
- 3. User sees: "Container timed out."
100
- 4. Queue unblocks, next message can proceed
99
+ 3. User sees: "I ran out of time before finishing. Partial work may be saved in the workspace — ask me to continue."
100
+ 4. A marker assistant message ("[System: this run was killed at the container time limit before replying. …]") is recorded in chat history, so the next run's agent knows the previous run was cut off and can check the workspace for partial work
101
+ 5. Queue unblocks, next message can proceed
101
102
 
102
103
  The host always injects a resolved **model chain** into the container (after `MERCURY_*` passthrough) so retries and fallbacks use the same policy Mercury loaded at startup:
103
104
 
@@ -113,7 +114,7 @@ Container failures are classified by `ContainerError`:
113
114
 
114
115
  | Reason | Exit Code | Cause | User Message |
115
116
  |--------|-----------|-------|--------------|
116
- | `timeout` | — | Exceeded `containerTimeoutMs` | "Container timed out." |
117
+ | `timeout` | — | Exceeded `containerTimeoutMs` | "I ran out of time before finishing. Partial work may be saved in the workspace — ask me to continue." |
117
118
  | `oom` | 137 | SIGKILL (OOM, resource limits, or manual kill) | "Container was killed (possibly out of memory)." |
118
119
  | `aborted` | — | User sent `stop` command | "Stopped current run." |
119
120
  | `error` | non-zero | Agent crashed or failed | *(error thrown, logged)* |
@@ -28,7 +28,7 @@ Message arrives
28
28
  |------|---------------------|-------------|
29
29
  | `system` | All | Internal system caller (scheduler, etc.) — not assignable |
30
30
  | `admin` | All | Full control over the space |
31
- | `member` | `prompt`, `prefs.get`, `media.receive`, `media.send` | Can chat, read space preferences, and exchange files (default for new users) |
31
+ | `member` | `prompt`, `prefs.get`, `media.receive`, `media.send`, `model.list` | Can chat, read space preferences, exchange files, and list models (default for new users) |
32
32
 
33
33
  Custom roles can be created by assigning permissions to any role name.
34
34
 
@@ -56,6 +56,8 @@ Custom roles can be created by assigning permissions to any role name.
56
56
  | `spaces.list` | View all spaces |
57
57
  | `spaces.rename` | Rename a space and link/unlink conversations |
58
58
  | `spaces.delete` | Delete current space and all related DB data |
59
+ | `model.list` | List the configured model chain (`mrctl model list`, chat `/model list`) |
60
+ | `model.switch` | Switch the active model for the space (`mrctl model switch`, chat `/model switch`) — takes effect on the next agent run |
59
61
  | `media.receive` | Incoming attachments are saved to `inbox/` and shown to the agent |
60
62
  | `media.send` | Outbox files produced on this caller's turn are delivered back to the chat |
61
63
 
@@ -28,19 +28,38 @@ npm i -g @googleworkspace/cli
28
28
  gws auth login
29
29
  ```
30
30
 
31
- 2. Export credentials once:
31
+ 2. Export the credentials and put the JSON (as a single line) into `.env`:
32
32
 
33
33
  ```bash
34
- gws auth export --unmasked > .mercury/global/gws-credentials.json
34
+ gws auth export --unmasked
35
35
  ```
36
36
 
37
- 3. Add to `.env`:
38
-
39
37
  ```bash
40
- MERCURY_GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/home/mercury/.pi/agent/gws-credentials.json
38
+ MERCURY_GWS_CREDENTIALS_JSON={"client_id":"...","client_secret":"...","refresh_token":"...","type":"authorized_user"}
41
39
  ```
42
40
 
43
- (You can also use `MERCURY_GOOGLE_WORKSPACE_CLI_TOKEN`, but it expires quickly.)
41
+ 3. Restart Mercury. The extension's `before_container` hook passes the JSON
42
+ into the inner container, and the skill materializes it as a credentials
43
+ file at runtime.
44
+
45
+ > **Do not use the file-based path.** Older versions of this README said to
46
+ > export to `.mercury/global/gws-credentials.json` and set
47
+ > `MERCURY_GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE`. That no longer works: the
48
+ > runtime mounts the global dir into the inner container entry-by-entry via an
49
+ > allowlist (`PI_AGENT_RESOURCE_ENTRIES` in `container-runner.ts`), and the
50
+ > credentials file is not on it — the container never sees it.
51
+ >
52
+ > **Windows/PowerShell note:** if you do redirect `gws auth export` output to a
53
+ > file, PowerShell's `>` writes UTF-16 — invalid for JSON readers. Use
54
+ > `gws auth export --unmasked | Out-File -Encoding utf8 <path>` instead.
55
+ >
56
+ > (`MERCURY_GOOGLE_WORKSPACE_CLI_TOKEN` also exists, but it expires quickly.)
57
+
58
+ ## Refreshing credentials
59
+
60
+ Re-run `gws auth login` + `gws auth export --unmasked`, replace the
61
+ `MERCURY_GWS_CREDENTIALS_JSON` value in `.env` (the refresh token changes),
62
+ and restart Mercury.
44
63
 
45
64
  ## Verify
46
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mercury-agent",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",
@@ -53,6 +53,14 @@ mrctl config set <key> <value> # Set config value
53
53
  # context.mode (clear|context), context.reply_chain_depth (1-50)
54
54
  ```
55
55
 
56
+ ### Models
57
+ ```bash
58
+ mrctl model list # List configured models (active one marked)
59
+ mrctl model switch <N|MODEL_ID> # Switch active model — takes effect on the NEXT agent run
60
+ ```
61
+
62
+ **Note:** A switch never changes the run you are in — say so when reporting it. Switching requires the `model.switch` permission (admin by default); a 403 means the caller must ask an admin.
63
+
56
64
  ### Spaces
57
65
  ```bash
58
66
  mrctl spaces list # List all spaces with names (admin-only)
@@ -63,7 +63,7 @@ function extractText(rawMessage?: proto.IMessage | null): string {
63
63
  );
64
64
  }
65
65
 
66
- function getContextInfo(
66
+ export function getContextInfo(
67
67
  rawMessage?: proto.IMessage | null,
68
68
  ): proto.IContextInfo | undefined {
69
69
  const message = normalizeMessageContent(rawMessage ?? undefined);
@@ -72,6 +72,9 @@ function getContextInfo(
72
72
  message.extendedTextMessage?.contextInfo ||
73
73
  message.imageMessage?.contextInfo ||
74
74
  message.videoMessage?.contextInfo ||
75
+ message.ptvMessage?.contextInfo ||
76
+ message.audioMessage?.contextInfo ||
77
+ message.stickerMessage?.contextInfo ||
75
78
  message.documentMessage?.contextInfo ||
76
79
  message.buttonsResponseMessage?.contextInfo ||
77
80
  message.templateButtonReplyMessage?.contextInfo ||
@@ -1,12 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import {
4
- normalizeMessageContent,
5
- type proto,
6
- type WAMessage,
7
- } from "@whiskeysockets/baileys";
3
+ import type { proto, WAMessage } from "@whiskeysockets/baileys";
8
4
  import type { Message } from "chat";
9
- import type { WhatsAppBaileysAdapter } from "../adapters/whatsapp.js";
5
+ import {
6
+ getContextInfo,
7
+ type WhatsAppBaileysAdapter,
8
+ } from "../adapters/whatsapp.js";
10
9
  import {
11
10
  detectWhatsAppMedia,
12
11
  downloadQuotedMedia,
@@ -97,14 +96,7 @@ export class WhatsAppBridge implements PlatformBridge {
97
96
  }
98
97
 
99
98
  if (attachments.length === 0) {
100
- const content = normalizeMessageContent(rawMsg.message ?? undefined);
101
- const contextInfo =
102
- content?.extendedTextMessage?.contextInfo ||
103
- content?.audioMessage?.contextInfo ||
104
- content?.imageMessage?.contextInfo ||
105
- content?.videoMessage?.contextInfo ||
106
- content?.documentMessage?.contextInfo ||
107
- content?.stickerMessage?.contextInfo;
99
+ const contextInfo = getContextInfo(rawMsg.message);
108
100
  if (contextInfo?.quotedMessage) {
109
101
  if (await ctx.isOverQuota()) {
110
102
  logger.warn(
package/src/cli/mrctl.ts CHANGED
@@ -82,6 +82,7 @@ Built-in commands:
82
82
  mrctl prefs list|get|set|delete
83
83
  mrctl roles list|grant|revoke
84
84
  mrctl permissions show|set
85
+ mrctl model list|switch <N|MODEL_ID> (switch takes effect on the NEXT agent run)
85
86
  mrctl spaces list|name [<name>]|delete (delete: CURRENT space only, takes no argument)
86
87
  mrctl conversations list
87
88
  mrctl mute <platform-user-id> <duration> [--reason <reason>]
@@ -405,6 +406,49 @@ async function main() {
405
406
  break;
406
407
  }
407
408
 
409
+ case "model": {
410
+ if (!sub) usage();
411
+ switch (sub) {
412
+ case "list": {
413
+ const data = (await api("GET", "/api/model")) as {
414
+ models: Array<{
415
+ index: number;
416
+ provider: string;
417
+ model: string;
418
+ active: boolean;
419
+ }>;
420
+ };
421
+ if (data.models.length === 0) {
422
+ process.stdout.write("No models configured.\n");
423
+ break;
424
+ }
425
+ // Same shape as chat /model list so the agent can relay it verbatim
426
+ process.stdout.write("Configured models:\n");
427
+ for (const m of data.models) {
428
+ process.stdout.write(
429
+ ` [${m.index}] ${m.provider} / ${m.model}${m.active ? " ← active" : ""}\n`,
430
+ );
431
+ }
432
+ break;
433
+ }
434
+ case "switch": {
435
+ const target = requireArg(args, 2, "target (<N|MODEL_ID>)");
436
+ const data = (await api("POST", "/api/model/switch", { target })) as {
437
+ provider: string;
438
+ model: string;
439
+ note: string;
440
+ };
441
+ process.stdout.write(
442
+ `Switched to ${data.provider} / ${data.model}. ${data.note}\n`,
443
+ );
444
+ break;
445
+ }
446
+ default:
447
+ fatal(`Unknown model subcommand: ${sub}`);
448
+ }
449
+ break;
450
+ }
451
+
408
452
  case "conversations": {
409
453
  const action = sub ?? "list";
410
454
  switch (action) {
package/src/core/api.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  extensions,
16
16
  media,
17
17
  messages,
18
+ model,
18
19
  mutes,
19
20
  permissions,
20
21
  prefs,
@@ -114,6 +115,7 @@ export function createApiApp(apiCtx: ApiContext): Hono<Env> {
114
115
  app.route("/conversations", conversations);
115
116
  app.route("/media", media);
116
117
  app.route("/messages", messages);
118
+ app.route("/model", model);
117
119
  app.route("/mutes", mutes);
118
120
  app.route("/ext", extensions);
119
121
  app.route("/connections", connections);
@@ -0,0 +1,55 @@
1
+ import type { ModelLeg } from "../config.js";
2
+
3
+ /**
4
+ * Shared resolution logic for the model chain — the single source of truth
5
+ * behind both the chat `/model list|switch` command (runtime.ts) and the
6
+ * `/api/model` route (routes/model.ts). Semantics live here so the two
7
+ * surfaces cannot drift.
8
+ */
9
+
10
+ /**
11
+ * Index of the active leg in the chain: the leg named by the stored
12
+ * `model.active` value (`provider:model`) when it still exists in the chain,
13
+ * otherwise leg 0. A stale value (host config changed since the switch) falls
14
+ * back to the primary leg, matching what container start does when it ignores
15
+ * an unknown override.
16
+ *
17
+ * Returns -1 only for an empty chain.
18
+ */
19
+ export function resolveActiveIndex(
20
+ chain: ModelLeg[],
21
+ activeRaw: string | null,
22
+ ): number {
23
+ if (chain.length === 0) return -1;
24
+ if (activeRaw) {
25
+ const idx = chain.findIndex(
26
+ (l) => `${l.provider}:${l.model}` === activeRaw,
27
+ );
28
+ if (idx !== -1) return idx;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ /**
34
+ * Resolve a switch target to a chain leg, or null if it names none.
35
+ * Accepted forms, in resolution order:
36
+ * 1. 1-based index into the chain ("2") — numeric parse wins over a model
37
+ * literally named like an in-range number
38
+ * 2. bare model id ("claude-opus-4-8")
39
+ * 3. provider-qualified id ("anthropic:claude-opus-4-8")
40
+ */
41
+ export function resolveSwitchTarget(
42
+ chain: ModelLeg[],
43
+ arg: string,
44
+ ): ModelLeg | null {
45
+ // Strict digits only: parseInt("2junk") is 2, and a typo silently switching
46
+ // to a real leg is exactly the failure this validated path exists to prevent.
47
+ if (/^\d+$/.test(arg)) {
48
+ const num = Number.parseInt(arg, 10);
49
+ if (num >= 1 && num <= chain.length) return chain[num - 1];
50
+ }
51
+ return (
52
+ chain.find((l) => l.model === arg || `${l.provider}:${l.model}` === arg) ??
53
+ null
54
+ );
55
+ }
@@ -34,6 +34,10 @@ const BUILT_IN_PERMISSIONS = new Set([
34
34
  "media.receive",
35
35
  /** Outbox files produced on this caller's turn are delivered back. */
36
36
  "media.send",
37
+ /** List the configured model chain (/api/model, chat /model list). */
38
+ "model.list",
39
+ /** Switch the active model (/api/model/switch, chat /model switch); admin-only by default. */
40
+ "model.switch",
37
41
  /** Host Text-to-Speech (/api/tts); admin-only by default. */
38
42
  "tts.synthesize",
39
43
  /** Mute/unmute users and list mutes; admin-only by default. */
@@ -194,6 +198,9 @@ const DEFAULT_MEMBER_PERMISSIONS = new Set([
194
198
  "prefs.get",
195
199
  "media.receive",
196
200
  "media.send",
201
+ // Read-only, like prefs.get: lets the agent answer "which models do I have?"
202
+ // for any caller. model.switch is deliberately absent — admin-only default.
203
+ "model.list",
197
204
  ]);
198
205
 
199
206
  /**
@@ -201,7 +208,7 @@ const DEFAULT_MEMBER_PERMISSIONS = new Set([
201
208
  * with extension-registered defaults.
202
209
  *
203
210
  * - `admin` and `system` get all permissions (built-in + extension)
204
- * - `member` gets `prompt`, `prefs.get`, `media.receive`, `media.send`, plus any extension permissions that list "member" in defaultRoles
211
+ * - `member` gets `prompt`, `prefs.get`, `media.receive`, `media.send`, `model.list`, plus any extension permissions that list "member" in defaultRoles
205
212
  * - Other roles get extension permissions that list them in defaultRoles
206
213
  */
207
214
  function getDefaultPermissions(role: string): Set<string> {
@@ -8,6 +8,7 @@ export { conversations } from "./conversations.js";
8
8
  export { extensions } from "./extensions.js";
9
9
  export { media } from "./media.js";
10
10
  export { messages } from "./messages.js";
11
+ export { model } from "./model.js";
11
12
  export { mutes } from "./mutes.js";
12
13
  export { prefs } from "./prefs.js";
13
14
  export { permissions, roles } from "./roles.js";
@@ -0,0 +1,68 @@
1
+ import { Hono } from "hono";
2
+ import { checkPerm, type Env, getApiCtx, getAuth } from "../api-types.js";
3
+ import { resolveActiveIndex, resolveSwitchTarget } from "../model-command.js";
4
+
5
+ export const model = new Hono<Env>();
6
+
7
+ model.get("/", (c) => {
8
+ const { spaceId } = getAuth(c);
9
+ const denied = checkPerm(c, "model.list");
10
+ if (denied) return denied;
11
+
12
+ const { db, config } = getApiCtx(c);
13
+ const chain = config.resolvedModelChain;
14
+ const activeIdx = resolveActiveIndex(
15
+ chain,
16
+ db.getSpaceConfig(spaceId, "model.active"),
17
+ );
18
+
19
+ return c.json({
20
+ models: chain.map((leg, i) => ({
21
+ // 1-based to match chat /model list and the switch target form
22
+ index: i + 1,
23
+ provider: leg.provider,
24
+ model: leg.model,
25
+ active: i === activeIdx,
26
+ })),
27
+ });
28
+ });
29
+
30
+ model.post("/switch", async (c) => {
31
+ const { spaceId, callerId } = getAuth(c);
32
+ const denied = checkPerm(c, "model.switch");
33
+ if (denied) return denied;
34
+
35
+ const { db, config } = getApiCtx(c);
36
+ const body = await c.req
37
+ .json<{ target?: string }>()
38
+ .catch(() => ({}) as { target?: string });
39
+
40
+ const target = typeof body.target === "string" ? body.target.trim() : "";
41
+ if (!target) return c.json({ error: "Missing target" }, 400);
42
+
43
+ const chain = config.resolvedModelChain;
44
+ if (chain.length === 0) {
45
+ return c.json({ error: "No models configured." }, 400);
46
+ }
47
+
48
+ const leg = resolveSwitchTarget(chain, target);
49
+ if (!leg) {
50
+ return c.json(
51
+ { error: "Model not found. Use mrctl model list to see your options." },
52
+ 400,
53
+ );
54
+ }
55
+
56
+ db.setSpaceConfig(
57
+ spaceId,
58
+ "model.active",
59
+ `${leg.provider}:${leg.model}`,
60
+ callerId,
61
+ );
62
+
63
+ return c.json({
64
+ provider: leg.provider,
65
+ model: leg.model,
66
+ note: "Takes effect on the next agent run; the current run keeps its model.",
67
+ });
68
+ });
@@ -33,6 +33,7 @@ import {
33
33
  resolveRecipientSpaceId,
34
34
  } from "./direct-send.js";
35
35
  import { gateIncomingMedia, gateOutgoingMedia } from "./media-gate.js";
36
+ import { resolveActiveIndex, resolveSwitchTarget } from "./model-command.js";
36
37
  import { hasPermission, resolveRole } from "./permissions.js";
37
38
  import { getActiveProfilePrompt } from "./profiles.js";
38
39
  import { RateLimiter } from "./rate-limiter.js";
@@ -587,6 +588,7 @@ export class MercuryCoreRuntime {
587
588
  message.spaceId,
588
589
  route.command,
589
590
  route.callerId,
591
+ route.role,
590
592
  route.verb,
591
593
  route.arg,
592
594
  {
@@ -803,6 +805,7 @@ export class MercuryCoreRuntime {
803
805
  spaceId: string,
804
806
  command: string,
805
807
  callerId: string,
808
+ role: string,
806
809
  verb?: string,
807
810
  arg?: string,
808
811
  conversationContext?: { platform: string; externalId: string },
@@ -833,7 +836,7 @@ export class MercuryCoreRuntime {
833
836
  return formatHelp();
834
837
  }
835
838
  case "model":
836
- return this.executeModelsCommand(spaceId, callerId, verb, arg);
839
+ return this.executeModelsCommand(spaceId, callerId, role, verb, arg);
837
840
  case "spaces":
838
841
  return this.executeSpacesCommand(
839
842
  spaceId,
@@ -854,6 +857,7 @@ export class MercuryCoreRuntime {
854
857
  private executeModelsCommand(
855
858
  spaceId: string,
856
859
  callerId: string,
860
+ role: string,
857
861
  verb?: string,
858
862
  arg?: string,
859
863
  ): string {
@@ -865,19 +869,18 @@ export class MercuryCoreRuntime {
865
869
 
866
870
  switch (verb) {
867
871
  case "list": {
872
+ // Deliberately NOT gated on model.list: chat list is read-only, members
873
+ // hold model.list by default anyway, and pre-gate behavior is kept
874
+ // byte-identical. Only the API surface enforces model.list (an admin
875
+ // revoking it blocks mrctl/API listing but not chat).
868
876
  if (chain.length === 0) return "No models configured.";
869
877
  const activeRaw = this.db.getSpaceConfig(spaceId, "model.active");
870
- const activeFound = activeRaw
871
- ? chain.some((l) => `${l.provider}:${l.model}` === activeRaw)
872
- : false;
878
+ const activeIdx = resolveActiveIndex(chain, activeRaw);
873
879
  const lines = ["Configured models:"];
874
880
  for (let i = 0; i < chain.length; i++) {
875
881
  const leg = chain[i];
876
- const isActive = activeFound
877
- ? activeRaw === `${leg.provider}:${leg.model}`
878
- : i === 0;
879
882
  lines.push(
880
- ` [${i + 1}] ${leg.provider} / ${leg.model}${isActive ? " ← active" : ""}`,
883
+ ` [${i + 1}] ${leg.provider} / ${leg.model}${i === activeIdx ? " ← active" : ""}`,
881
884
  );
882
885
  }
883
886
  return lines.join("\n");
@@ -890,20 +893,17 @@ export class MercuryCoreRuntime {
890
893
  return "`/model active` has been removed — `/model list` marks the active model.";
891
894
 
892
895
  case "switch": {
896
+ // The router only admin-gates slash commands in GROUP chats; in DMs any
897
+ // prompt-holder reaches this point. Same permission as /api/model/switch
898
+ // so the two surfaces cannot disagree.
899
+ if (!hasPermission(this.db, spaceId, role, "model.switch")) {
900
+ return "Switching models requires the 'model.switch' permission — ask an admin.";
901
+ }
893
902
  if (!arg)
894
903
  return "Usage: /model switch <N|MODEL_ID>. Use /model list to see options.";
895
904
  if (chain.length === 0) return "No models configured.";
896
905
 
897
- let leg: (typeof chain)[number] | undefined;
898
- const num = Number.parseInt(arg, 10);
899
- if (!Number.isNaN(num) && num >= 1 && num <= chain.length) {
900
- leg = chain[num - 1];
901
- } else {
902
- leg = chain.find(
903
- (l) => l.model === arg || `${l.provider}:${l.model}` === arg,
904
- );
905
- }
906
-
906
+ const leg = resolveSwitchTarget(chain, arg);
907
907
  if (!leg)
908
908
  return "Model not found. Use /model list to see your options.";
909
909
 
@@ -1870,6 +1870,19 @@ export class MercuryCoreRuntime {
1870
1870
  });
1871
1871
  } catch (err) {
1872
1872
  this.db.updateMessageRunMeta(userMessageId, userTurnRunMeta(undefined));
1873
+ // Timeout kills the container mid-task with no assistant reply, so the
1874
+ // next run's history would show an unanswered turn with no explanation.
1875
+ // Record why, so the agent knows the run was cut off rather than
1876
+ // guessing — workspace files written before the kill survive.
1877
+ if (err instanceof ContainerError && err.reason === "timeout") {
1878
+ this.db.addMessage(
1879
+ spaceId,
1880
+ "assistant",
1881
+ "[System: this run was killed at the container time limit before replying. Partial work may exist in the workspace.]",
1882
+ undefined,
1883
+ userMessageId,
1884
+ );
1885
+ }
1873
1886
  throw err;
1874
1887
  }
1875
1888
 
@@ -8,8 +8,8 @@
8
8
  * space switch them to another locale via the `messages.locale` config key.
9
9
  *
10
10
  * Resolution chain: per-space → `@global` → `config.messagesLocale` → `"en"`.
11
- * The `en` strings are byte-identical to the original literals tests and
12
- * downstream matching depend on them.
11
+ * The `en` strings are pinned byte-for-byte by testsdownstream matching
12
+ * depends on them, so change them deliberately and update the test together.
13
13
  */
14
14
 
15
15
  import type { AppConfig } from "../config.js";
@@ -63,7 +63,8 @@ export const SYSTEM_MESSAGES: Record<
63
63
  attachment_failed:
64
64
  "Could not use your attachment (media disabled, over the size limit, or download failed). Check MERCURY_MEDIA_ENABLED and logs.",
65
65
  run_stopped: "Stopped current run.",
66
- container_timeout: "Container timed out.",
66
+ container_timeout:
67
+ "I ran out of time before finishing. Partial work may be saved in the workspace — ask me to continue.",
67
68
  container_oom: "Container was killed (possibly out of memory).",
68
69
  no_permission_prompt:
69
70
  "You don't have permission to use the agent in this group.",
@@ -114,7 +115,8 @@ export const SYSTEM_MESSAGES: Record<
114
115
  attachment_failed:
115
116
  "לא ניתן היה להשתמש בקובץ המצורף (מדיה מושבתת, חריגה ממגבלת הגודל, או שההורדה נכשלה). בדקו את MERCURY_MEDIA_ENABLED ואת הלוגים.",
116
117
  run_stopped: "הריצה הנוכחית הופסקה.",
117
- container_timeout: "זמן הריצה של הקונטיינר פג.",
118
+ container_timeout:
119
+ "נגמר לי הזמן לפני שסיימתי. ייתכן שעבודה חלקית נשמרה בסביבת העבודה — בקשו ממני להמשיך.",
118
120
  container_oom: "הקונטיינר הופסק (ייתכן שבשל מחסור בזיכרון).",
119
121
  no_permission_prompt: "אין לכם הרשאה להשתמש בסוכן בקבוצה זו.",
120
122
  no_permission_slash: "אין לכם הרשאה להשתמש ב-'/{command}'.",