@botcord/daemon 0.2.77 → 0.2.79

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.
Files changed (66) hide show
  1. package/dist/agent-discovery.d.ts +6 -0
  2. package/dist/agent-discovery.js +6 -0
  3. package/dist/attention-policy-fetcher.d.ts +14 -0
  4. package/dist/attention-policy-fetcher.js +59 -0
  5. package/dist/cloud-daemon.js +8 -0
  6. package/dist/cloud-gateway-runtime.d.ts +29 -0
  7. package/dist/cloud-gateway-runtime.js +122 -0
  8. package/dist/daemon-config-map.d.ts +6 -0
  9. package/dist/daemon-config-map.js +5 -4
  10. package/dist/daemon.d.ts +3 -0
  11. package/dist/daemon.js +32 -7
  12. package/dist/gateway/channels/botcord.js +29 -9
  13. package/dist/gateway/channels/login-session.d.ts +12 -0
  14. package/dist/gateway/channels/login-session.js +20 -2
  15. package/dist/gateway/channels/sanitize.d.ts +5 -18
  16. package/dist/gateway/channels/sanitize.js +5 -54
  17. package/dist/gateway/channels/text-split.d.ts +5 -11
  18. package/dist/gateway/channels/text-split.js +5 -31
  19. package/dist/gateway/dispatcher.d.ts +7 -1
  20. package/dist/gateway/dispatcher.js +88 -8
  21. package/dist/gateway/gateway.d.ts +16 -1
  22. package/dist/gateway/gateway.js +21 -0
  23. package/dist/gateway/policy-resolver.js +17 -9
  24. package/dist/gateway/runtimes/deepseek-tui.js +86 -19
  25. package/dist/gateway/types.d.ts +12 -57
  26. package/dist/gateway-control.js +18 -9
  27. package/dist/provision.d.ts +9 -3
  28. package/dist/provision.js +181 -9
  29. package/dist/room-recovery-context.d.ts +11 -0
  30. package/dist/room-recovery-context.js +97 -0
  31. package/dist/runtime-models.d.ts +17 -0
  32. package/dist/runtime-models.js +953 -0
  33. package/dist/runtime-route-options.d.ts +7 -0
  34. package/dist/runtime-route-options.js +45 -0
  35. package/package.json +2 -2
  36. package/src/__tests__/attention-policy-fetcher.test.ts +67 -0
  37. package/src/__tests__/cloud-gateway-runtime.test.ts +127 -0
  38. package/src/__tests__/daemon-config-map.test.ts +26 -1
  39. package/src/__tests__/gateway-control.test.ts +136 -0
  40. package/src/__tests__/policy-resolver.test.ts +20 -0
  41. package/src/__tests__/provision.test.ts +124 -0
  42. package/src/__tests__/runtime-discovery.test.ts +68 -9
  43. package/src/__tests__/runtime-models.test.ts +333 -0
  44. package/src/agent-discovery.ts +9 -0
  45. package/src/attention-policy-fetcher.ts +87 -0
  46. package/src/cloud-daemon.ts +8 -0
  47. package/src/cloud-gateway-runtime.ts +171 -0
  48. package/src/daemon-config-map.ts +17 -4
  49. package/src/daemon.ts +38 -9
  50. package/src/gateway/__tests__/botcord-channel.test.ts +97 -0
  51. package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +207 -1
  52. package/src/gateway/__tests__/dispatcher.test.ts +56 -0
  53. package/src/gateway/channels/botcord.ts +32 -8
  54. package/src/gateway/channels/login-session.ts +20 -2
  55. package/src/gateway/channels/sanitize.ts +8 -66
  56. package/src/gateway/channels/text-split.ts +5 -27
  57. package/src/gateway/dispatcher.ts +123 -27
  58. package/src/gateway/gateway.ts +29 -0
  59. package/src/gateway/policy-resolver.ts +20 -9
  60. package/src/gateway/runtimes/deepseek-tui.ts +86 -19
  61. package/src/gateway/types.ts +31 -59
  62. package/src/gateway-control.ts +21 -9
  63. package/src/provision.ts +202 -11
  64. package/src/room-recovery-context.ts +131 -0
  65. package/src/runtime-models.ts +972 -0
  66. package/src/runtime-route-options.ts +52 -0
@@ -0,0 +1,333 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import {
6
+ discoverRuntimeModelCatalog,
7
+ parseCodexModelCatalog,
8
+ parseDeepseekModelList,
9
+ parseKimiConfigModels,
10
+ parseKimiRuntimeParameters,
11
+ } from "../runtime-models.js";
12
+
13
+ describe("runtime model discovery parsers", () => {
14
+ it("parses visible Codex models and trims heavyweight catalog fields", () => {
15
+ const models = parseCodexModelCatalog(
16
+ JSON.stringify({
17
+ models: [
18
+ {
19
+ slug: "gpt-5.5",
20
+ display_name: "GPT-5.5",
21
+ visibility: "list",
22
+ supported_in_api: true,
23
+ default_reasoning_level: "medium",
24
+ supported_reasoning_levels: [{ effort: "low" }, { effort: "medium" }],
25
+ base_instructions: "large prompt text should not be copied into metadata",
26
+ },
27
+ {
28
+ slug: "internal-hidden",
29
+ display_name: "Internal",
30
+ visibility: "hide",
31
+ },
32
+ ],
33
+ }),
34
+ );
35
+
36
+ expect(models).toEqual([
37
+ {
38
+ id: "gpt-5.5",
39
+ displayName: "GPT-5.5",
40
+ provider: "openai",
41
+ source: "cli",
42
+ metadata: {
43
+ supportedInApi: true,
44
+ defaultReasoningLevel: "medium",
45
+ supportedReasoningLevels: ["low", "medium"],
46
+ },
47
+ parameters: [
48
+ {
49
+ id: "reasoning_effort",
50
+ displayName: "Reasoning effort",
51
+ type: "enum",
52
+ flag: "-c model_reasoning_effort=<value>",
53
+ values: ["low", "medium"],
54
+ defaultValue: "medium",
55
+ source: "cli",
56
+ },
57
+ ],
58
+ },
59
+ ]);
60
+ });
61
+
62
+ it("parses Codex CLI cache descriptions", () => {
63
+ expect(
64
+ parseCodexModelCatalog(
65
+ JSON.stringify({
66
+ models: [
67
+ {
68
+ slug: "gpt-cache",
69
+ description: "Cached GPT",
70
+ visibility: "list",
71
+ supported_in_api: true,
72
+ },
73
+ ],
74
+ }),
75
+ ),
76
+ ).toEqual([
77
+ {
78
+ id: "gpt-cache",
79
+ displayName: "Cached GPT",
80
+ provider: "openai",
81
+ source: "cli",
82
+ metadata: { supportedInApi: true },
83
+ },
84
+ ]);
85
+ });
86
+
87
+ it("uses the Codex CLI model cache when live discovery is unavailable", () => {
88
+ const tmp = mkdtempSync(path.join(tmpdir(), "daemon-codex-cache-"));
89
+ const prevHome = process.env.HOME;
90
+ const prevCodexHome = process.env.CODEX_HOME;
91
+ try {
92
+ const codexHome = path.join(tmp, "codex-home");
93
+ mkdirSync(codexHome, { recursive: true });
94
+ process.env.HOME = path.join(tmp, "home");
95
+ process.env.CODEX_HOME = codexHome;
96
+ writeFileSync(
97
+ path.join(codexHome, "models_cache.json"),
98
+ JSON.stringify({
99
+ models: [
100
+ {
101
+ slug: "gpt-cache",
102
+ description: "Cached GPT",
103
+ visibility: "list",
104
+ },
105
+ ],
106
+ }),
107
+ );
108
+
109
+ const catalog = discoverRuntimeModelCatalog({
110
+ id: "codex",
111
+ displayName: "Codex",
112
+ binary: "codex",
113
+ supportsRun: true,
114
+ result: { available: true, path: path.join(tmp, "missing-codex") },
115
+ });
116
+
117
+ expect(catalog.models).toEqual([
118
+ {
119
+ id: "gpt-cache",
120
+ displayName: "Cached GPT",
121
+ provider: "openai",
122
+ source: "cli",
123
+ },
124
+ ]);
125
+ } finally {
126
+ if (prevHome === undefined) delete process.env.HOME;
127
+ else process.env.HOME = prevHome;
128
+ if (prevCodexHome === undefined) delete process.env.CODEX_HOME;
129
+ else process.env.CODEX_HOME = prevCodexHome;
130
+ rmSync(tmp, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ it("falls back to built-in Codex models and persists the runtime catalog cache", () => {
135
+ const tmp = mkdtempSync(path.join(tmpdir(), "daemon-runtime-catalog-"));
136
+ const prevHome = process.env.HOME;
137
+ const prevCodexHome = process.env.CODEX_HOME;
138
+ const prevCacheDir = process.env.BOTCORD_RUNTIME_CATALOG_CACHE_DIR;
139
+ try {
140
+ const codexHome = path.join(tmp, "codex-home");
141
+ const cacheDir = path.join(tmp, "catalog-cache");
142
+ mkdirSync(codexHome, { recursive: true });
143
+ process.env.HOME = path.join(tmp, "home");
144
+ process.env.CODEX_HOME = codexHome;
145
+ process.env.BOTCORD_RUNTIME_CATALOG_CACHE_DIR = cacheDir;
146
+
147
+ const catalog = discoverRuntimeModelCatalog({
148
+ id: "codex",
149
+ displayName: "Codex",
150
+ binary: "codex",
151
+ supportsRun: true,
152
+ result: { available: true, path: path.join(tmp, "missing-codex") },
153
+ });
154
+
155
+ expect(catalog.models?.map((m) => m.id)).toContain("gpt-5.2");
156
+ expect(readdirSync(cacheDir)).toEqual(["codex.json"]);
157
+ const payload = JSON.parse(readFileSync(path.join(cacheDir, "codex.json"), "utf8"));
158
+ expect(payload.runtimeId).toBe("codex");
159
+ expect(payload.catalog.models.map((m: { id: string }) => m.id)).toContain("gpt-5.2");
160
+ } finally {
161
+ if (prevHome === undefined) delete process.env.HOME;
162
+ else process.env.HOME = prevHome;
163
+ if (prevCodexHome === undefined) delete process.env.CODEX_HOME;
164
+ else process.env.CODEX_HOME = prevCodexHome;
165
+ if (prevCacheDir === undefined) delete process.env.BOTCORD_RUNTIME_CATALOG_CACHE_DIR;
166
+ else process.env.BOTCORD_RUNTIME_CATALOG_CACHE_DIR = prevCacheDir;
167
+ rmSync(tmp, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ it("parses DeepSeek model list output", () => {
172
+ expect(
173
+ parseDeepseekModelList(
174
+ [
175
+ "deepseek-v4-pro (deepseek)",
176
+ "gpt-4.1-mini (openai)",
177
+ "deepseek-coder:1.3b (ollama)",
178
+ ].join("\n"),
179
+ ),
180
+ ).toEqual([
181
+ {
182
+ id: "deepseek-v4-pro",
183
+ displayName: "deepseek-v4-pro",
184
+ provider: "deepseek",
185
+ source: "cli",
186
+ },
187
+ {
188
+ id: "gpt-4.1-mini",
189
+ displayName: "gpt-4.1-mini",
190
+ provider: "openai",
191
+ source: "cli",
192
+ },
193
+ {
194
+ id: "deepseek-coder:1.3b",
195
+ displayName: "deepseek-coder:1.3b",
196
+ provider: "ollama",
197
+ source: "cli",
198
+ },
199
+ ]);
200
+ });
201
+
202
+ it("parses Kimi models from config.toml", () => {
203
+ expect(
204
+ parseKimiConfigModels(
205
+ [
206
+ 'default_model = "kimi-code/kimi-for-coding"',
207
+ "default_thinking = true",
208
+ "",
209
+ '[models."kimi-code/kimi-for-coding"]',
210
+ 'provider = "managed:kimi-code"',
211
+ 'model = "kimi-for-coding"',
212
+ "max_context_size = 262144",
213
+ 'capabilities = ["thinking", "video_in", "image_in"]',
214
+ 'display_name = "Kimi-k2.6"',
215
+ ].join("\n"),
216
+ ),
217
+ ).toEqual([
218
+ {
219
+ id: "kimi-code/kimi-for-coding",
220
+ source: "config",
221
+ isDefault: true,
222
+ provider: "managed:kimi-code",
223
+ displayName: "Kimi-k2.6",
224
+ contextLength: 262144,
225
+ capabilities: ["thinking", "video_in", "image_in"],
226
+ metadata: { model: "kimi-for-coding" },
227
+ parameters: [
228
+ {
229
+ id: "thinking",
230
+ displayName: "Thinking",
231
+ type: "boolean",
232
+ flag: "--thinking/--no-thinking",
233
+ defaultValue: true,
234
+ source: "config",
235
+ },
236
+ ],
237
+ },
238
+ ]);
239
+ });
240
+
241
+ it("parses Kimi runtime parameters from config.toml", () => {
242
+ expect(
243
+ parseKimiRuntimeParameters(
244
+ [
245
+ 'default_model = "kimi-code/kimi-for-coding"',
246
+ "default_thinking = true",
247
+ "default_yolo = false",
248
+ "default_plan_mode = false",
249
+ "show_thinking_stream = true",
250
+ "max_steps_per_turn = 1000",
251
+ "max_retries_per_step = 3",
252
+ "max_ralph_iterations = 0",
253
+ "reserved_context_size = 50000",
254
+ ].join("\n"),
255
+ ),
256
+ ).toEqual([
257
+ {
258
+ id: "model",
259
+ displayName: "Default model",
260
+ type: "string",
261
+ flag: "-m, --model",
262
+ defaultValue: "kimi-code/kimi-for-coding",
263
+ source: "config",
264
+ },
265
+ {
266
+ id: "thinking",
267
+ displayName: "Thinking",
268
+ type: "boolean",
269
+ flag: "--thinking/--no-thinking",
270
+ defaultValue: true,
271
+ source: "config",
272
+ },
273
+ {
274
+ id: "show_thinking_stream",
275
+ displayName: "Show thinking stream",
276
+ type: "boolean",
277
+ defaultValue: true,
278
+ source: "config",
279
+ },
280
+ {
281
+ id: "yolo",
282
+ displayName: "Auto approve",
283
+ type: "boolean",
284
+ flag: "--yolo, --yes, -y",
285
+ defaultValue: false,
286
+ source: "config",
287
+ },
288
+ {
289
+ id: "plan_mode",
290
+ displayName: "Plan mode",
291
+ type: "boolean",
292
+ flag: "--plan",
293
+ defaultValue: false,
294
+ source: "config",
295
+ },
296
+ {
297
+ id: "max_steps_per_turn",
298
+ displayName: "Max steps per turn",
299
+ type: "integer",
300
+ flag: "--max-steps-per-turn",
301
+ defaultValue: 1000,
302
+ minimum: 1,
303
+ source: "config",
304
+ },
305
+ {
306
+ id: "max_retries_per_step",
307
+ displayName: "Max retries per step",
308
+ type: "integer",
309
+ flag: "--max-retries-per-step",
310
+ defaultValue: 3,
311
+ minimum: 1,
312
+ source: "config",
313
+ },
314
+ {
315
+ id: "max_ralph_iterations",
316
+ displayName: "Max Ralph iterations",
317
+ type: "integer",
318
+ flag: "--max-ralph-iterations",
319
+ defaultValue: 0,
320
+ minimum: -1,
321
+ source: "config",
322
+ },
323
+ {
324
+ id: "reserved_context_size",
325
+ displayName: "Reserved context size",
326
+ type: "integer",
327
+ defaultValue: 50000,
328
+ minimum: 0,
329
+ source: "config",
330
+ },
331
+ ]);
332
+ });
333
+ });
@@ -36,6 +36,12 @@ export interface DiscoveredAgentCredential {
36
36
  * in that case.
37
37
  */
38
38
  runtime?: string;
39
+ /** Runtime model id/alias selected for this agent. */
40
+ runtimeModel?: string;
41
+ /** Runtime reasoning effort selected for this agent. */
42
+ reasoningEffort?: string;
43
+ /** Kimi-style thinking toggle selected for this agent. */
44
+ thinking?: boolean;
39
45
  /** Working directory cached alongside `runtime`. */
40
46
  cwd?: string;
41
47
  /** OpenClaw gateway profile name from credentials (only meaningful for openclaw-acp). */
@@ -181,6 +187,9 @@ export function discoverAgentCredentials(
181
187
  };
182
188
  if (creds.displayName) entry.displayName = creds.displayName;
183
189
  if (creds.runtime) entry.runtime = creds.runtime;
190
+ if (creds.runtimeModel) entry.runtimeModel = creds.runtimeModel;
191
+ if (creds.reasoningEffort) entry.reasoningEffort = creds.reasoningEffort;
192
+ if (typeof creds.thinking === "boolean") entry.thinking = creds.thinking;
184
193
  if (creds.cwd) entry.cwd = creds.cwd;
185
194
  if (creds.openclawGateway) entry.openclawGateway = creds.openclawGateway;
186
195
  if (creds.openclawAgent) entry.openclawAgent = creds.openclawAgent;
@@ -0,0 +1,87 @@
1
+ import {
2
+ BotCordClient,
3
+ defaultCredentialsFile,
4
+ loadStoredCredentials,
5
+ updateCredentialsToken,
6
+ } from "@botcord/protocol-core";
7
+ import type { DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
8
+
9
+ interface CachedClient {
10
+ client: BotCordClient;
11
+ credentialsPath: string;
12
+ }
13
+
14
+ export interface AttentionPolicyFetcherOptions {
15
+ credentialPathByAgentId: Map<string, string>;
16
+ defaultCredentialsPath?: string;
17
+ hubBaseUrl?: string;
18
+ log?: {
19
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
20
+ };
21
+ }
22
+
23
+ export type AttentionPolicyFetcher = (args: {
24
+ agentId: string;
25
+ roomId?: string | null;
26
+ }) => Promise<DaemonAttentionPolicy | undefined>;
27
+
28
+ export function createAttentionPolicyFetcher(
29
+ opts: AttentionPolicyFetcherOptions,
30
+ ): AttentionPolicyFetcher {
31
+ const clients = new Map<string, CachedClient>();
32
+
33
+ function getClient(agentId: string): BotCordClient | null {
34
+ const existing = clients.get(agentId);
35
+ if (existing) return existing.client;
36
+
37
+ const credentialsPath =
38
+ opts.credentialPathByAgentId.get(agentId) ??
39
+ opts.defaultCredentialsPath ??
40
+ defaultCredentialsFile(agentId);
41
+
42
+ try {
43
+ const creds = loadStoredCredentials(credentialsPath);
44
+ const client = new BotCordClient({
45
+ hubUrl: opts.hubBaseUrl ?? creds.hubUrl,
46
+ agentId: creds.agentId,
47
+ keyId: creds.keyId,
48
+ privateKey: creds.privateKey,
49
+ ...(creds.token ? { token: creds.token } : {}),
50
+ ...(creds.tokenExpiresAt !== undefined
51
+ ? { tokenExpiresAt: creds.tokenExpiresAt }
52
+ : {}),
53
+ });
54
+ client.onTokenRefresh = (token, expiresAt) => {
55
+ try {
56
+ updateCredentialsToken(credentialsPath, token, expiresAt);
57
+ } catch {
58
+ // Persistence failures are non-fatal; the next refresh retries.
59
+ }
60
+ };
61
+ clients.set(agentId, { client, credentialsPath });
62
+ return client;
63
+ } catch (err) {
64
+ opts.log?.warn("daemon.attention-policy.client-init-failed", {
65
+ agentId,
66
+ credentialsPath,
67
+ error: err instanceof Error ? err.message : String(err),
68
+ });
69
+ return null;
70
+ }
71
+ }
72
+
73
+ return async ({ agentId, roomId }) => {
74
+ const client = getClient(agentId);
75
+ if (!client) return undefined;
76
+ try {
77
+ return await client.getAttentionPolicy({ roomId });
78
+ } catch (err) {
79
+ opts.log?.warn("daemon.attention-policy.fetch-failed", {
80
+ agentId,
81
+ roomId: roomId ?? null,
82
+ error: err instanceof Error ? err.message : String(err),
83
+ });
84
+ return undefined;
85
+ }
86
+ };
87
+ }
@@ -32,6 +32,7 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
32
32
  import { readWorkingMemorySnapshot } from "./working-memory.js";
33
33
  import { createRoomStaticContextBuilder } from "./room-context.js";
34
34
  import { createRoomContextFetcher } from "./room-context-fetcher.js";
35
+ import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
35
36
  import { composeBotCordUserTurn } from "./turn-text.js";
36
37
  import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
37
38
  import { scanMention } from "./mention-scan.js";
@@ -143,6 +144,12 @@ export async function startCloudDaemon(
143
144
  fetchRoomInfo: roomContextFetcher,
144
145
  log: logger,
145
146
  });
147
+ const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
148
+ credentialPathByAgentId,
149
+ hubBaseUrl: cloudCfg.hubUrl,
150
+ limit: 20,
151
+ log: logger,
152
+ });
146
153
 
147
154
  type PerAgentBuilder = (
148
155
  msg: GatewayInboundMessage,
@@ -258,6 +265,7 @@ export async function startCloudDaemon(
258
265
  turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
259
266
  buildSystemContext,
260
267
  buildMemoryContext,
268
+ buildRuntimeRecoveryContext,
261
269
  onInbound,
262
270
  onTurnComplete,
263
271
  composeUserTurn: composeBotCordUserTurn,
@@ -0,0 +1,171 @@
1
+ import {
2
+ RUNTIME_FRAME_TYPES,
3
+ type GatewayInboundFrame,
4
+ } from "@botcord/protocol-core";
5
+
6
+ import type {
7
+ ChannelAdapter,
8
+ ChannelSendContext,
9
+ ChannelSendResult,
10
+ ChannelStatusSnapshot,
11
+ Gateway,
12
+ GatewayInboundMessage,
13
+ GatewayLogger,
14
+ } from "./gateway/index.js";
15
+
16
+ export interface CloudGatewayRuntimeResult {
17
+ accepted: boolean;
18
+ eventId: string;
19
+ gatewayId: string;
20
+ agentId: string;
21
+ conversationId: string;
22
+ turnId: string;
23
+ outbound?: {
24
+ finalText: string;
25
+ providerMessageId?: string | null;
26
+ };
27
+ error?: {
28
+ code: string;
29
+ message: string;
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Execute one ingress-originated runtime frame through the normal Gateway
35
+ * dispatcher while keeping provider I/O outside the cloud sandbox.
36
+ *
37
+ * The temporary channel is scoped to this call and implements only the
38
+ * dispatcher-facing send/status surface. Its send() method captures the
39
+ * final runtime reply; the Hub relay converts that into
40
+ * gateway_outbound_complete for gateway-ingress, which then calls the real
41
+ * provider API.
42
+ */
43
+ export async function handleCloudGatewayRuntimeInbound(
44
+ gateway: Gateway,
45
+ frame: GatewayInboundFrame,
46
+ log?: GatewayLogger,
47
+ ): Promise<CloudGatewayRuntimeResult> {
48
+ if (frame.type !== RUNTIME_FRAME_TYPES.GATEWAY_INBOUND) {
49
+ return rejected(frame, "bad_frame_type", `unsupported frame type "${frame.type}"`);
50
+ }
51
+ if (!frame.gateway_id || !frame.agent_id || !frame.event_id) {
52
+ return rejected(frame, "bad_frame", "gateway_id, agent_id and event_id are required");
53
+ }
54
+ if (frame.message.accountId !== frame.agent_id) {
55
+ return rejected(frame, "account_mismatch", "message.accountId does not match frame.agent_id");
56
+ }
57
+ if (frame.message.channel !== frame.gateway_id) {
58
+ return rejected(frame, "channel_mismatch", "message.channel does not match frame.gateway_id");
59
+ }
60
+
61
+ let accepted = false;
62
+ let outboundText: string | null = null;
63
+ let providerMessageId: string | null | undefined;
64
+ const channel = createRuntimeRelayChannel({
65
+ id: frame.gateway_id,
66
+ provider: frame.provider,
67
+ accountId: frame.agent_id,
68
+ onSend: async (ctx) => {
69
+ outboundText = ctx.message.text ?? "";
70
+ providerMessageId = ctx.message.traceId ?? null;
71
+ return { providerMessageId };
72
+ },
73
+ });
74
+
75
+ const message: GatewayInboundMessage = {
76
+ ...frame.message,
77
+ raw: {
78
+ source_type: "cloud_gateway_ingress",
79
+ provider: frame.provider,
80
+ event_id: frame.event_id,
81
+ gateway_id: frame.gateway_id,
82
+ },
83
+ };
84
+
85
+ try {
86
+ await gateway.injectInboundThrough(message, channel, {
87
+ accept: async () => {
88
+ accepted = true;
89
+ },
90
+ });
91
+ } catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ log?.warn("cloud gateway runtime dispatch failed", {
94
+ eventId: frame.event_id,
95
+ gatewayId: frame.gateway_id,
96
+ agentId: frame.agent_id,
97
+ error: message,
98
+ });
99
+ return rejected(frame, "dispatch_failed", message);
100
+ }
101
+
102
+ return {
103
+ accepted,
104
+ eventId: frame.event_id,
105
+ gatewayId: frame.gateway_id,
106
+ agentId: frame.agent_id,
107
+ conversationId: frame.message.conversation.id,
108
+ turnId: `turn_${frame.event_id}`,
109
+ ...(outboundText !== null
110
+ ? {
111
+ outbound: {
112
+ finalText: outboundText,
113
+ providerMessageId: providerMessageId ?? null,
114
+ },
115
+ }
116
+ : {}),
117
+ ...(!accepted
118
+ ? { error: { code: "not_accepted", message: "dispatcher did not accept inbound" } }
119
+ : {}),
120
+ };
121
+ }
122
+
123
+ function createRuntimeRelayChannel(opts: {
124
+ id: string;
125
+ provider: string;
126
+ accountId: string;
127
+ onSend: (ctx: ChannelSendContext) => Promise<ChannelSendResult>;
128
+ }): ChannelAdapter {
129
+ let lastSendAt: number | undefined;
130
+ return {
131
+ id: opts.id,
132
+ type: opts.provider,
133
+ async start() {
134
+ return undefined;
135
+ },
136
+ async stop() {
137
+ return undefined;
138
+ },
139
+ async send(ctx) {
140
+ lastSendAt = Date.now();
141
+ return opts.onSend(ctx);
142
+ },
143
+ status(): ChannelStatusSnapshot {
144
+ return {
145
+ channel: opts.id,
146
+ accountId: opts.accountId,
147
+ running: true,
148
+ connected: true,
149
+ authorized: true,
150
+ provider: opts.provider as ChannelStatusSnapshot["provider"],
151
+ ...(lastSendAt ? { lastSendAt } : {}),
152
+ };
153
+ },
154
+ };
155
+ }
156
+
157
+ function rejected(
158
+ frame: Partial<GatewayInboundFrame>,
159
+ code: string,
160
+ message: string,
161
+ ): CloudGatewayRuntimeResult {
162
+ return {
163
+ accepted: false,
164
+ eventId: frame.event_id ?? "",
165
+ gatewayId: frame.gateway_id ?? "",
166
+ agentId: frame.agent_id ?? "",
167
+ conversationId: frame.message?.conversation.id ?? "",
168
+ turnId: frame.event_id ? `turn_${frame.event_id}` : "turn_unknown",
169
+ error: { code, message },
170
+ };
171
+ }
@@ -18,10 +18,20 @@ import type {
18
18
  import { resolveAgentIds } from "./config.js";
19
19
  import { agentWorkspaceDir } from "./agent-workspace.js";
20
20
  import { log as daemonLog } from "./log.js";
21
+ import {
22
+ buildRuntimeSelectionExtraArgs,
23
+ mergeRuntimeExtraArgs,
24
+ } from "./runtime-route-options.js";
21
25
 
22
26
  /** Per-agent metadata cached from credentials, used by `buildManagedRoutes`. */
23
27
  export interface AgentRuntimeMeta {
24
28
  runtime?: string;
29
+ /** Runtime model id/alias selected for this agent. */
30
+ runtimeModel?: string;
31
+ /** Runtime reasoning effort selected for this agent. */
32
+ reasoningEffort?: string;
33
+ /** Kimi-style thinking toggle selected for this agent. */
34
+ thinking?: boolean;
25
35
  cwd?: string;
26
36
  /** OpenClaw gateway profile name to lookup in the registry. */
27
37
  openclawGateway?: string;
@@ -346,10 +356,13 @@ export function buildManagedRoutes(
346
356
  match: { accountId: agentId },
347
357
  runtime,
348
358
  cwd: meta.cwd || agentWorkspaceDir(agentId),
349
- // Inherit defaultRoute's extraArgs so synthesized per-agent routes
350
- // pick up operator-wide flags (e.g. `--permission-mode bypassPermissions`)
351
- // that would otherwise apply only to agents listed in `cfg.routes[]`.
352
- ...(defaultRoute.extraArgs ? { extraArgs: defaultRoute.extraArgs.slice() } : {}),
359
+ ...(() => {
360
+ const extraArgs = mergeRuntimeExtraArgs(
361
+ defaultRoute.extraArgs,
362
+ buildRuntimeSelectionExtraArgs(runtime, meta),
363
+ );
364
+ return extraArgs ? { extraArgs } : {};
365
+ })(),
353
366
  };
354
367
  if (runtime === "openclaw-acp") {
355
368
  // Per RFC §3.4: prefer credentials, fall back to defaultRoute.gateway.