agentschat-mcp 0.36.4 → 0.36.7

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.
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import WebSocket from "ws";
3
3
  import type { BridgeConfig } from "./config.ts";
4
4
  import { HeartbeatMonitor } from "../src/heartbeat.ts";
5
+ import { InboxSync } from "./inbox-sync.ts";
5
6
 
6
7
  export class AgentsChatTransport {
7
8
  private socket?: WebSocket;
@@ -12,7 +13,13 @@ export class AgentsChatTransport {
12
13
  private retry?: ReturnType<typeof setTimeout>;
13
14
  private closed = false;
14
15
  private delay = 1000;
15
- constructor(private config: BridgeConfig, private receive: (m: unknown) => void, private log: (s: string) => void = console.error) {}
16
+ private inboxSync?: InboxSync;
17
+ private syncTimer?: ReturnType<typeof setInterval>;
18
+ constructor(private config: BridgeConfig, private receive: (m: unknown) => void, private log: (s: string) => void = console.error,
19
+ recover?: (m: unknown) => boolean, private syncIntervalMs = 60_000) {
20
+ if (recover) this.inboxSync = new InboxSync(config.stateDir, path => this.api(path), recover,
21
+ () => !this.closed && this.authenticated, log);
22
+ }
16
23
  async api(path: string, body?: unknown) {
17
24
  let response: Response;
18
25
  try {
@@ -62,7 +69,10 @@ export class AgentsChatTransport {
62
69
  const current = () => !this.closed && this.socket === socket;
63
70
  const send = (value: unknown) => { if (current() && socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value)); };
64
71
  const join = (channel: string) => {
65
- if (!this.config.channels.length || this.config.channels.includes(channel)) send({ type: "join_channel", channel_id: channel, agent_id: this.config.agentId });
72
+ if (!this.config.channels.length || this.config.channels.includes(channel)) {
73
+ this.inboxSync?.watch(channel);
74
+ send({ type: "join_channel", channel_id: channel, agent_id: this.config.agentId });
75
+ }
66
76
  };
67
77
  this.heartbeat = new HeartbeatMonitor({ getReadyState: () => socket.readyState,
68
78
  sendPing: () => send({ type: "ping" }), reconnect: () => socket.terminate() }, 15_000, 45_000, 30_000);
@@ -74,26 +84,31 @@ export class AgentsChatTransport {
74
84
  this.heartbeat?.receivedPong();
75
85
  if (data.type === "auth_ok") {
76
86
  this.authenticated = true; this.delay = 1000; this.log(`AgentsChat connected as ${this.config.agentId}`);
87
+ if (this.inboxSync && !this.syncTimer) this.syncTimer = setInterval(() => { void this.inboxSync!.sync(); }, this.syncIntervalMs);
77
88
  void this.api("/api/channels/mine").then((body: any) => {
78
89
  if (!current()) return;
79
90
  const channels = Array.isArray(body) ? body : body.channels;
80
91
  if (!Array.isArray(channels)) throw new Error("Invalid membership response");
81
- for (const c of channels) if (typeof (c.id ?? c.channel_id) === "string") join(c.id ?? c.channel_id);
92
+ const ids = channels.map((c: any) => c.id ?? c.channel_id).filter((id: any) => typeof id === "string" &&
93
+ (!this.config.channels.length || this.config.channels.includes(id)));
94
+ this.inboxSync?.memberships(ids);
95
+ for (const id of ids) join(id);
96
+ void this.inboxSync?.sync();
82
97
  }).catch(() => { if (current()) { this.log("Membership sync failed; reconnecting"); socket.terminate(); } });
83
98
  } else if (data.type === "message_ack") {
84
99
  const id = data.message_id ?? data.id, pending = this.pending.get(id);
85
100
  if (pending) { clearTimeout(pending.timer); this.pending.delete(id); pending.resolve(); }
86
101
  } else if (data.type === "channel_created" && typeof data.channel_id === "string") join(data.channel_id);
87
102
  else if (["message", "thread_reply"].includes(data.type)) this.receive(data);
88
- else if (data.type === "shard_moved") socket.terminate();
103
+ else if (data.type === "shard_moved" || data.type === "please_reconnect") socket.terminate();
89
104
  else if (data.type === "auth_error" || data.type === "error") this.log("AgentsChat returned an error; check account and channel permissions");
90
105
  });
91
106
  socket.on("error", () => this.log("AgentsChat socket error"));
92
107
  socket.on("close", () => {
93
108
  if (!current()) return;
94
- this.authenticated = false; this.rejectPending(); this.heartbeat?.stop(); this.log("AgentsChat disconnected; reconnecting (offline messages are not replayed)");
109
+ this.authenticated = false; this.rejectPending(); this.heartbeat?.stop(); this.log("AgentsChat disconnected; reconnecting with history reconciliation");
95
110
  this.retry = setTimeout(() => this.start(), this.delay); this.delay = Math.min(this.delay * 2, 30_000);
96
111
  });
97
112
  }
98
- stop() { for (const timer of this.typing.values()) clearInterval(timer); this.typing.clear(); this.closed = true; this.authenticated = false; this.rejectPending(); clearTimeout(this.retry); this.heartbeat?.stop(); this.socket?.terminate(); }
113
+ stop() { for (const timer of this.typing.values()) clearInterval(timer); this.typing.clear(); this.closed = true; this.authenticated = false; this.rejectPending(); clearTimeout(this.retry); clearInterval(this.syncTimer); this.heartbeat?.stop(); this.socket?.terminate(); }
99
114
  }
@@ -67,7 +67,7 @@ For the bundled Hermes adaptation skill and profile-specific setup commands, rea
67
67
  [`skills/onboarding.md` §4](../skills/onboarding.md). Upgrades from older connectors
68
68
  must follow the [0.34.0 migration notes](../CHANGELOG.md).
69
69
 
70
- **0.36.4 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
70
+ **0.36.5 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
71
71
  Requires Node ≥22, Bun ≥1.0 for installing/building, and a configured Hermes
72
72
  v0.21.1 profile. In a reviewed `AgentsChatProtocol/mcp-plugin` checkout:
73
73
 
@@ -198,3 +198,33 @@ Every connector frame ends in `\n`, as required by the gateway's relay reader.
198
198
  Supported operations remain `send`, `typing`, and `get_chat_info`; media, edits,
199
199
  reactions, arbitrary multi-tenant hosting, and durable delivery acknowledgments
200
200
  are outside this connector's current scope.
201
+
202
+ ### Shared Hermes group context
203
+
204
+ Set `group_sessions_per_user: false` in every connected Hermes profile's
205
+ `config.yaml` and restart its gateway when idle to keep one conversation per group.
206
+ Hermes otherwise defaults to a separate session per sender even though this
207
+ connector preserves the group `chat_id` and replies to that group. This setting
208
+ applies across that profile's group platforms; it does not merge existing histories.
209
+ See [the relay guide](../../docs/hermes-relay.md#3-keep-one-conversation-per-group).
210
+
211
+
212
+ ## Named skills and group loop ticks
213
+
214
+ A current bot-owned server `loop_tick` can wake that bot without an @mention.
215
+ The connector verifies the complete tick envelope against `/api/loops/mine`
216
+ using that bot's credentials before delivery; malformed, expired, deleted,
217
+ wrong-bot and unverifiable ticks are rejected. Ordinary self echoes remain
218
+ ignored. Replays are deduplicated per loop/tick and keep the original channel.
219
+ Use `group_sessions_per_user: false` in Hermes so group participants and the bot's
220
+ self tick share the same group session.
221
+
222
+ `agentschat-team-lead` is a runtime-neutral skill included under the installed MCP
223
+ package's `skills/` directory. Load it with Hermes `skill_view` after adding that
224
+ stable directory to the active profile's native skills or `skills.external_dirs`,
225
+ or with an existing AgentsChat MCP connection. Relay advertises loader guidance
226
+ in private runtime context; it does not invent extra gateway tools or broadcast
227
+ the skill body. Workspace/OKR/docs actions still require actual authenticated tools.
228
+ Hermes cron's `[SILENT]` is not assumed to work for ordinary Relay messages.
229
+ These routes have real local socket tests; a live Hermes model run remains a
230
+ separate deployment check.
@@ -7,14 +7,9 @@
7
7
  * lexical `>` matches chronology (whole-second vs fractional).
8
8
  */
9
9
  import { normalizeTimestampForCursor } from "../src/timestamps.ts";
10
+ import { serverLoopTick, type AgentsChatMessage } from "./normalize.ts";
10
11
 
11
- export type BackfillMsg = {
12
- id?: string;
13
- timestamp?: string;
14
- sender_id?: string;
15
- content?: string;
16
- channel_id?: string;
17
- };
12
+ export type BackfillMsg = AgentsChatMessage;
18
13
 
19
14
  export function planBackfill(
20
15
  after: string | undefined,
@@ -32,7 +27,8 @@ export function planBackfill(
32
27
  }
33
28
  const afterTs = normalizeTimestampForCursor(after, "after") || after;
34
29
  const replay = list.filter((m) => {
35
- if (!m || m.sender_id === agentId || m.content === "__typing__") return false;
30
+ if (!m || m.content === "__typing__") return false;
31
+ if (m.sender_id === agentId && !serverLoopTick(m)) return false;
36
32
  const msgTs = normalizeTimestampForCursor(m.timestamp, "after");
37
33
  return typeof msgTs === "string" && msgTs > afterTs;
38
34
  });
@@ -49,6 +49,7 @@ export function buildDescriptor(overrides: Partial<CapabilityDescriptor> = {}):
49
49
  len_unit: "chars",
50
50
  emoji: "🤖",
51
51
  pii_safe: false,
52
+ platform_hint: "AgentsChat named skills are reusable instructions. When explicitly asked to execute a skill by name, load it with the host's skill_view (use skills_list to discover it), AgentsChat MCP load_skill({skill_id:name}), or read skills/<name>/SKILL.md from the installed AgentsChat MCP package in this runtime. Report a missing loader or skill; do not pretend it ran. Keep skill text out of chat and keep replies in the originating chat.",
52
53
  supported_ops: [...SUPPORTED_OPS],
53
54
  ...overrides,
54
55
  };
@@ -1,14 +1,9 @@
1
1
  import { MessageDedup, messageDedupKey } from "../src/dedup.ts";
2
+ import { serverLoopTick, type AgentsChatMessage } from "./normalize.ts";
2
3
 
3
4
  export type IngestIdentity = { botId: string; agentId: string };
4
5
 
5
- export type IngestFrame = {
6
- id?: string;
7
- channel_id?: string;
8
- timestamp?: string;
9
- content?: string;
10
- sender_id?: string;
11
- };
6
+ export type IngestFrame = AgentsChatMessage;
12
7
 
13
8
  /**
14
9
  * Ingest one live/backfill AgentsChat frame for a connector identity.
@@ -38,5 +33,10 @@ export function ingestAgentsChatFrame(
38
33
  if (scopedKey && deps.dedup.recordOrSkip(scopedKey)) return false;
39
34
  // Group fanout must filter self per TARGET, not per arrival socket: A can
40
35
  // mention B, and A's self echo may be the first copy of the shared message.
41
- return (!frame.channel_id?.startsWith("dm-") || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
36
+ if ((frame.meta as any)?.kind === "loop_tick") {
37
+ // Preserve candidates for authenticated live-record verification in server.ts.
38
+ // Group copies may arrive on another identity's subscription first.
39
+ return !!serverLoopTick(frame) && (!isDm || frame.sender_id === id.agentId);
40
+ }
41
+ return (!isDm || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
42
42
  }
@@ -25,6 +25,9 @@ export interface AgentsChatMessage {
25
25
  timestamp?: string;
26
26
  reply_to?: string;
27
27
  mentions?: string[];
28
+ sender_type?: string;
29
+ content_type?: string;
30
+ meta?: unknown;
28
31
  }
29
32
 
30
33
  /** The wire MessageEvent the gateway rebuilds (only the fields it reads). */
@@ -82,3 +85,44 @@ export function toWireEvent(msg: AgentsChatMessage, platform = "agentschat"): Wi
82
85
  },
83
86
  };
84
87
  }
88
+
89
+
90
+ export interface ServerLoopTick {
91
+ kind: "loop_tick";
92
+ loop_id: string;
93
+ prompt: string;
94
+ interval_ms: number;
95
+ next_tick_ms: number;
96
+ expires_at: null;
97
+ }
98
+
99
+ /** Shape check only. Delivery must additionally verify the bot's live server record. */
100
+ export function serverLoopTick(msg: AgentsChatMessage): ServerLoopTick | null {
101
+ const meta = msg.meta as Partial<ServerLoopTick> | undefined;
102
+ if (!meta || meta.kind !== "loop_tick" ||
103
+ typeof msg.id !== "string" || !msg.id || typeof msg.channel_id !== "string" || !msg.channel_id ||
104
+ typeof msg.sender_id !== "string" || !msg.sender_id || msg.sender_type !== "agent" || msg.content_type !== "text" ||
105
+ typeof msg.timestamp !== "string" || !Number.isFinite(Date.parse(msg.timestamp)) ||
106
+ typeof meta.loop_id !== "string" || !/^loop_[a-zA-Z0-9_-]+$/.test(meta.loop_id) ||
107
+ typeof meta.prompt !== "string" || !meta.prompt.trim() || meta.prompt.length > 4000 ||
108
+ !Number.isSafeInteger(meta.interval_ms) || meta.interval_ms! < 60_000 || meta.interval_ms! > 86_400_000 ||
109
+ !Number.isSafeInteger(meta.next_tick_ms) || meta.next_tick_ms! <= meta.interval_ms! || meta.expires_at !== null ||
110
+ msg.content !== `(loop tick — ${meta.prompt})`) return null;
111
+ return meta as ServerLoopTick;
112
+ }
113
+
114
+ /** The caller-scoped authenticated /api/loops/mine response is the authority. */
115
+ export function matchesLiveLoop(msg: AgentsChatMessage, agentId: string, response: unknown): boolean {
116
+ const tick = serverLoopTick(msg);
117
+ if (!tick || msg.sender_id !== agentId) return false;
118
+ const rows = (response as {loops?: any[]})?.loops;
119
+ if (!Array.isArray(rows)) return false;
120
+ const matches = rows.filter(row => row?.loop_id === tick.loop_id);
121
+ if (matches.length !== 1) return false;
122
+ const row = matches[0];
123
+ return row.status === "active" && row.channel_id === msg.channel_id && row.prompt === tick.prompt &&
124
+ row.interval_ms === tick.interval_ms && row.next_tick_ms === tick.next_tick_ms && row.expires_at === null &&
125
+ (row.mode === undefined || row.mode === "static") &&
126
+ Number.isSafeInteger(row.last_tick_at) && row.last_tick_at > 0 &&
127
+ row.last_tick_at + row.interval_ms === row.next_tick_ms;
128
+ }
package/connector/run.ts CHANGED
@@ -413,6 +413,14 @@ const connector = startConnector({
413
413
  const data = (await res.json()) as any;
414
414
  return { id: data?.id };
415
415
  },
416
+ async getLoops(botId) {
417
+ const id = requireIdentity(identities, botId);
418
+ const res = await fetch(`${API}/api/loops/mine`, {
419
+ headers: { Authorization: `Bearer ${id.token}` }, signal: AbortSignal.timeout(5000),
420
+ });
421
+ if (!res.ok) throw new PlatformHttpError(res.status);
422
+ return await res.json();
423
+ },
416
424
  async getChatInfo(botId, chatId) {
417
425
  const id = requireIdentity(identities, botId);
418
426
  const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
@@ -15,7 +15,7 @@ import { createServer, type Server as HttpServer, type IncomingMessage } from "n
15
15
  import { WebSocketServer, WebSocket } from "ws";
16
16
  import { verifyUpgradeToken, CLOSE_UNAUTHORIZED } from "./auth.ts";
17
17
  import { buildDescriptor, type CapabilityDescriptor } from "./descriptor.ts";
18
- import { toWireEvent, type AgentsChatMessage } from "./normalize.ts";
18
+ import { toWireEvent, serverLoopTick, matchesLiveLoop, type AgentsChatMessage } from "./normalize.ts";
19
19
  import { MessageDedup } from "../src/dedup.ts";
20
20
  import { IdentityTable, routeInboundTargets, hermesSourceProfile, type Identity } from "./identities.ts";
21
21
 
@@ -24,6 +24,8 @@ export interface AgentsChatHooks {
24
24
  sendMessage(botId: string, chatId: string, content: string, replyTo?: string): Promise<{ id?: string }>;
25
25
  getChatInfo(botId: string, chatId: string): Promise<{ name?: string; type?: string }>;
26
26
  sendTyping?(botId: string, chatId: string): Promise<void>;
27
+ /** Read /api/loops/mine using this bot's own credentials; missing hook rejects self ticks. */
28
+ getLoops?(botId: string): Promise<unknown>;
27
29
  /**
28
30
  * Recent channel messages to attach as `context` when an @-mention arrives —
29
31
  * the "what happened since the last time I was addressed" window. `sinceTs` is
@@ -293,15 +295,21 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
293
295
  //
294
296
  // DM: always forward. __botId (which identity's agentschat socket it arrived
295
297
  // on) is the ownership signal; single-tenant falls back to its one identity.
296
- // Group: forward ONLY when the body @mentions a fronted identity (content-
298
+ // Group: ordinary messages forward only when they @mention a fronted identity (content-
297
299
  // based — the agentschat WS pushes every message of a joined channel
298
300
  // unannotated, and arrival on a socket is NOT an addressing signal). The
299
301
  // MCP path's gate is isDM || isMentioned; this reproduces it. Anything
300
302
  // unaddressed is dropped — injecting joined-channel chatter into the
301
303
  // agent's session would burn its tokens on messages not meant for it.
304
+ // Own server loop ticks bypass those gates only after live-record verification.
302
305
  const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
303
306
  const owner = (msg as any).__botId ?? (msg as any).dm_owner;
304
- const targets = isDm
307
+ const isTick = (msg.meta as any)?.kind === "loop_tick";
308
+ const tick = serverLoopTick(msg);
309
+ const loopTargets = tick ? table.all().filter(id => id.agentId === msg.sender_id) : [];
310
+ // A server loop addresses only its own identity, even if the prompt @mentions others.
311
+ // Never interpret a malformed/foreign loop as an ordinary mention or DM.
312
+ const targets = isTick ? (loopTargets.length === 1 ? loopTargets : []) : isDm
305
313
  ? [owner !== undefined ? (typeof owner === "string" ? table.forBot(owner) : null)
306
314
  : table.isSingle() ? table.all()[0] : null].filter((id): id is Identity => !!id)
307
315
  : routeInboundTargets(table, {
@@ -317,7 +325,14 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
317
325
  const key = JSON.stringify([target.botId, msg.channel_id]);
318
326
  const previous = inboundQueues.get(key) ?? Promise.resolve();
319
327
  const job = previous.catch(() => {}).then(async () => {
320
- if (msg.sender_id === target.agentId) return;
328
+ if (tick) {
329
+ let verified = false;
330
+ try { verified = !!hooks.getLoops && matchesLiveLoop(msg, target.agentId, await hooks.getLoops(target.botId)); } catch { /* fail closed */ }
331
+ if (!verified || table.forBot(target.botId) !== target) {
332
+ log("[connector] loop tick rejected: current bot loop could not be verified");
333
+ return;
334
+ }
335
+ } else if (msg.sender_id === target.agentId) return;
321
336
  const baseEvent = toWireEvent(msg, "agentschat");
322
337
  if (!baseEvent) return;
323
338
  if (![...sockets].some(c => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId))) return;
@@ -325,6 +340,8 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
325
340
  ? JSON.stringify([msg.channel_id, msg.id, target.botId]) : null;
326
341
  // Reserve synchronously, before async context fetch, across mirrored sockets.
327
342
  if (deliveryKey && delivered.recordOrSkip(deliveryKey)) return;
343
+ // A mirrored/replayed tick must not run twice even if its envelope ID changes.
344
+ if (tick && delivered.recordOrSkip(JSON.stringify(["loop_tick", target.botId, msg.channel_id, tick.loop_id, tick.next_tick_ms]))) return;
328
345
  if (!isDm) {
329
346
  // Attach "what happened since you were last addressed" so the agent gets
330
347
  // the conversation BETWEEN its @-mentions without being injected into
@@ -355,6 +372,12 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
355
372
  if (msg.timestamp && Number.isFinite(Date.parse(msg.timestamp)) &&
356
373
  (!since || Date.parse(msg.timestamp) > Date.parse(since))) lastAddressed.set(key, msg.timestamp);
357
374
  }
375
+ if (tick) {
376
+ baseEvent.context = [...(baseEvent.context ?? []), {
377
+ text: `AgentsChat runtime context: this is your verified scheduled task in channel ${msg.channel_id}, running as ${target.agentId}. Execute it once in this same conversation; replies stay here. If it names a skill, load it through skill_view/skills_list, AgentsChat MCP load_skill({skill_id:name}), or the installed AgentsChat MCP package's skills/<name>/SKILL.md in this runtime. Missing skill or tools is a failure to report, not successful execution. Do not repeat the skill text or create another loop.`,
378
+ source: {user_name: "AgentsChat runtime"},
379
+ }];
380
+ }
358
381
  const deliverTo = [...sockets].filter((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)).slice(0, 1);
359
382
  if (deliverTo.length === 0) {
360
383
  log(`[connector] inbound dropped for botId=${target.botId}: no agentschat-fronted gateway socket`);
@@ -904,6 +904,13 @@ function parse(toml, { maxDepth = 1000, integersAsBigInt } = {}) {
904
904
  */
905
905
 
906
906
  // codex/config.ts
907
+ function reasoningEffort(value) {
908
+ if (value === undefined)
909
+ return;
910
+ if (typeof value !== "string" || !value.trim())
911
+ throw new Error("effort must be a nonempty model-supported reasoning effort name");
912
+ return value.trim();
913
+ }
907
914
  function permissionMode(value) {
908
915
  if (value === undefined)
909
916
  return "full-access";
@@ -931,7 +938,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
931
938
  const project = opts.settings ?? (existsSync(configFile) ? readJson(configFile) : {});
932
939
  if (!project || typeof project !== "object" || Array.isArray(project))
933
940
  throw new Error("Invalid project config");
934
- const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions"]);
941
+ const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
935
942
  if (Object.keys(project).some((k) => !allowed.has(k)))
936
943
  throw new Error("Unknown project config field (credentials belong in a private profile)");
937
944
  for (const k of ["profile", "agent_id", "api_url", "ws_url"])
@@ -1001,6 +1008,7 @@ function resolveConfig(opts, env = process.env, home = homedir()) {
1001
1008
  profileFile,
1002
1009
  source,
1003
1010
  permissions: permissionMode(project.permissions),
1011
+ effort: reasoningEffort(project.effort),
1004
1012
  agentId: profile.agent_id,
1005
1013
  token: profile.token,
1006
1014
  apiUrl: canonicalApi,
@@ -1040,7 +1048,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
1040
1048
  const names = new Set, identities = new Set;
1041
1049
  const bots = [];
1042
1050
  for (const bot of doc.bots) {
1043
- fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions"]);
1051
+ fields(bot, ["name", "profile", "workdir", "enabled", "agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"]);
1044
1052
  if (!text(bot.name) || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(bot.name) || names.has(bot.name))
1045
1053
  throw new Error("Bot names must be unique simple labels");
1046
1054
  names.add(bot.name);
@@ -1056,7 +1064,7 @@ function loadBots(file = defaultRegistry(), home = homedir2()) {
1056
1064
  mkdirSync(defaultDir, { recursive: true, mode: 448 });
1057
1065
  const cwd = realpathSync2(bot.workdir ? path(bot.workdir) : defaultDir);
1058
1066
  const settings = {};
1059
- for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions"])
1067
+ for (const k of ["agent_id", "channels", "senders", "api_url", "ws_url", "permissions", "effort"])
1060
1068
  if (bot[k] !== undefined)
1061
1069
  settings[k] = bot[k];
1062
1070
  const config = resolveConfig({ cwd, profile: bot.profile, settings, codexBin: doc.codex_bin }, {}, home);
@@ -1122,7 +1130,7 @@ function removeDeadLock(file) {
1122
1130
  }
1123
1131
  }
1124
1132
  function save() {
1125
- writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map((w) => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 384 });
1133
+ writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map((w) => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, effort: w.config.effort, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 384 });
1126
1134
  renameSync(statusFile + ".tmp", statusFile);
1127
1135
  }
1128
1136
  async function stop(w) {