@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
@@ -22,6 +22,12 @@ export interface DiscoveredAgentCredential {
22
22
  * in that case.
23
23
  */
24
24
  runtime?: string;
25
+ /** Runtime model id/alias selected for this agent. */
26
+ runtimeModel?: string;
27
+ /** Runtime reasoning effort selected for this agent. */
28
+ reasoningEffort?: string;
29
+ /** Kimi-style thinking toggle selected for this agent. */
30
+ thinking?: boolean;
25
31
  /** Working directory cached alongside `runtime`. */
26
32
  cwd?: string;
27
33
  /** OpenClaw gateway profile name from credentials (only meaningful for openclaw-acp). */
@@ -96,6 +96,12 @@ export function discoverAgentCredentials(opts = {}) {
96
96
  entry.displayName = creds.displayName;
97
97
  if (creds.runtime)
98
98
  entry.runtime = creds.runtime;
99
+ if (creds.runtimeModel)
100
+ entry.runtimeModel = creds.runtimeModel;
101
+ if (creds.reasoningEffort)
102
+ entry.reasoningEffort = creds.reasoningEffort;
103
+ if (typeof creds.thinking === "boolean")
104
+ entry.thinking = creds.thinking;
99
105
  if (creds.cwd)
100
106
  entry.cwd = creds.cwd;
101
107
  if (creds.openclawGateway)
@@ -0,0 +1,14 @@
1
+ import type { DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
2
+ export interface AttentionPolicyFetcherOptions {
3
+ credentialPathByAgentId: Map<string, string>;
4
+ defaultCredentialsPath?: string;
5
+ hubBaseUrl?: string;
6
+ log?: {
7
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
8
+ };
9
+ }
10
+ export type AttentionPolicyFetcher = (args: {
11
+ agentId: string;
12
+ roomId?: string | null;
13
+ }) => Promise<DaemonAttentionPolicy | undefined>;
14
+ export declare function createAttentionPolicyFetcher(opts: AttentionPolicyFetcherOptions): AttentionPolicyFetcher;
@@ -0,0 +1,59 @@
1
+ import { BotCordClient, defaultCredentialsFile, loadStoredCredentials, updateCredentialsToken, } from "@botcord/protocol-core";
2
+ export function createAttentionPolicyFetcher(opts) {
3
+ const clients = new Map();
4
+ function getClient(agentId) {
5
+ const existing = clients.get(agentId);
6
+ if (existing)
7
+ return existing.client;
8
+ const credentialsPath = opts.credentialPathByAgentId.get(agentId) ??
9
+ opts.defaultCredentialsPath ??
10
+ defaultCredentialsFile(agentId);
11
+ try {
12
+ const creds = loadStoredCredentials(credentialsPath);
13
+ const client = new BotCordClient({
14
+ hubUrl: opts.hubBaseUrl ?? creds.hubUrl,
15
+ agentId: creds.agentId,
16
+ keyId: creds.keyId,
17
+ privateKey: creds.privateKey,
18
+ ...(creds.token ? { token: creds.token } : {}),
19
+ ...(creds.tokenExpiresAt !== undefined
20
+ ? { tokenExpiresAt: creds.tokenExpiresAt }
21
+ : {}),
22
+ });
23
+ client.onTokenRefresh = (token, expiresAt) => {
24
+ try {
25
+ updateCredentialsToken(credentialsPath, token, expiresAt);
26
+ }
27
+ catch {
28
+ // Persistence failures are non-fatal; the next refresh retries.
29
+ }
30
+ };
31
+ clients.set(agentId, { client, credentialsPath });
32
+ return client;
33
+ }
34
+ catch (err) {
35
+ opts.log?.warn("daemon.attention-policy.client-init-failed", {
36
+ agentId,
37
+ credentialsPath,
38
+ error: err instanceof Error ? err.message : String(err),
39
+ });
40
+ return null;
41
+ }
42
+ }
43
+ return async ({ agentId, roomId }) => {
44
+ const client = getClient(agentId);
45
+ if (!client)
46
+ return undefined;
47
+ try {
48
+ return await client.getAttentionPolicy({ roomId });
49
+ }
50
+ catch (err) {
51
+ opts.log?.warn("daemon.attention-policy.fetch-failed", {
52
+ agentId,
53
+ roomId: roomId ?? null,
54
+ error: err instanceof Error ? err.message : String(err),
55
+ });
56
+ return undefined;
57
+ }
58
+ };
59
+ }
@@ -23,6 +23,7 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
23
23
  import { readWorkingMemorySnapshot } from "./working-memory.js";
24
24
  import { createRoomStaticContextBuilder } from "./room-context.js";
25
25
  import { createRoomContextFetcher } from "./room-context-fetcher.js";
26
+ import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
26
27
  import { composeBotCordUserTurn } from "./turn-text.js";
27
28
  import { PolicyResolver } from "./gateway/policy-resolver.js";
28
29
  import { scanMention } from "./mention-scan.js";
@@ -92,6 +93,12 @@ export async function startCloudDaemon(opts) {
92
93
  fetchRoomInfo: roomContextFetcher,
93
94
  log: logger,
94
95
  });
96
+ const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
97
+ credentialPathByAgentId,
98
+ hubBaseUrl: cloudCfg.hubUrl,
99
+ limit: 20,
100
+ log: logger,
101
+ });
95
102
  const scBuilders = new Map();
96
103
  const buildSystemContext = (message) => {
97
104
  const b = scBuilders.get(message.accountId);
@@ -180,6 +187,7 @@ export async function startCloudDaemon(opts) {
180
187
  turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
181
188
  buildSystemContext,
182
189
  buildMemoryContext,
190
+ buildRuntimeRecoveryContext,
183
191
  onInbound,
184
192
  onTurnComplete,
185
193
  composeUserTurn: composeBotCordUserTurn,
@@ -0,0 +1,29 @@
1
+ import { type GatewayInboundFrame } from "@botcord/protocol-core";
2
+ import type { Gateway, GatewayLogger } from "./gateway/index.js";
3
+ export interface CloudGatewayRuntimeResult {
4
+ accepted: boolean;
5
+ eventId: string;
6
+ gatewayId: string;
7
+ agentId: string;
8
+ conversationId: string;
9
+ turnId: string;
10
+ outbound?: {
11
+ finalText: string;
12
+ providerMessageId?: string | null;
13
+ };
14
+ error?: {
15
+ code: string;
16
+ message: string;
17
+ };
18
+ }
19
+ /**
20
+ * Execute one ingress-originated runtime frame through the normal Gateway
21
+ * dispatcher while keeping provider I/O outside the cloud sandbox.
22
+ *
23
+ * The temporary channel is scoped to this call and implements only the
24
+ * dispatcher-facing send/status surface. Its send() method captures the
25
+ * final runtime reply; the Hub relay converts that into
26
+ * gateway_outbound_complete for gateway-ingress, which then calls the real
27
+ * provider API.
28
+ */
29
+ export declare function handleCloudGatewayRuntimeInbound(gateway: Gateway, frame: GatewayInboundFrame, log?: GatewayLogger): Promise<CloudGatewayRuntimeResult>;
@@ -0,0 +1,122 @@
1
+ import { RUNTIME_FRAME_TYPES, } from "@botcord/protocol-core";
2
+ /**
3
+ * Execute one ingress-originated runtime frame through the normal Gateway
4
+ * dispatcher while keeping provider I/O outside the cloud sandbox.
5
+ *
6
+ * The temporary channel is scoped to this call and implements only the
7
+ * dispatcher-facing send/status surface. Its send() method captures the
8
+ * final runtime reply; the Hub relay converts that into
9
+ * gateway_outbound_complete for gateway-ingress, which then calls the real
10
+ * provider API.
11
+ */
12
+ export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
13
+ if (frame.type !== RUNTIME_FRAME_TYPES.GATEWAY_INBOUND) {
14
+ return rejected(frame, "bad_frame_type", `unsupported frame type "${frame.type}"`);
15
+ }
16
+ if (!frame.gateway_id || !frame.agent_id || !frame.event_id) {
17
+ return rejected(frame, "bad_frame", "gateway_id, agent_id and event_id are required");
18
+ }
19
+ if (frame.message.accountId !== frame.agent_id) {
20
+ return rejected(frame, "account_mismatch", "message.accountId does not match frame.agent_id");
21
+ }
22
+ if (frame.message.channel !== frame.gateway_id) {
23
+ return rejected(frame, "channel_mismatch", "message.channel does not match frame.gateway_id");
24
+ }
25
+ let accepted = false;
26
+ let outboundText = null;
27
+ let providerMessageId;
28
+ const channel = createRuntimeRelayChannel({
29
+ id: frame.gateway_id,
30
+ provider: frame.provider,
31
+ accountId: frame.agent_id,
32
+ onSend: async (ctx) => {
33
+ outboundText = ctx.message.text ?? "";
34
+ providerMessageId = ctx.message.traceId ?? null;
35
+ return { providerMessageId };
36
+ },
37
+ });
38
+ const message = {
39
+ ...frame.message,
40
+ raw: {
41
+ source_type: "cloud_gateway_ingress",
42
+ provider: frame.provider,
43
+ event_id: frame.event_id,
44
+ gateway_id: frame.gateway_id,
45
+ },
46
+ };
47
+ try {
48
+ await gateway.injectInboundThrough(message, channel, {
49
+ accept: async () => {
50
+ accepted = true;
51
+ },
52
+ });
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ log?.warn("cloud gateway runtime dispatch failed", {
57
+ eventId: frame.event_id,
58
+ gatewayId: frame.gateway_id,
59
+ agentId: frame.agent_id,
60
+ error: message,
61
+ });
62
+ return rejected(frame, "dispatch_failed", message);
63
+ }
64
+ return {
65
+ accepted,
66
+ eventId: frame.event_id,
67
+ gatewayId: frame.gateway_id,
68
+ agentId: frame.agent_id,
69
+ conversationId: frame.message.conversation.id,
70
+ turnId: `turn_${frame.event_id}`,
71
+ ...(outboundText !== null
72
+ ? {
73
+ outbound: {
74
+ finalText: outboundText,
75
+ providerMessageId: providerMessageId ?? null,
76
+ },
77
+ }
78
+ : {}),
79
+ ...(!accepted
80
+ ? { error: { code: "not_accepted", message: "dispatcher did not accept inbound" } }
81
+ : {}),
82
+ };
83
+ }
84
+ function createRuntimeRelayChannel(opts) {
85
+ let lastSendAt;
86
+ return {
87
+ id: opts.id,
88
+ type: opts.provider,
89
+ async start() {
90
+ return undefined;
91
+ },
92
+ async stop() {
93
+ return undefined;
94
+ },
95
+ async send(ctx) {
96
+ lastSendAt = Date.now();
97
+ return opts.onSend(ctx);
98
+ },
99
+ status() {
100
+ return {
101
+ channel: opts.id,
102
+ accountId: opts.accountId,
103
+ running: true,
104
+ connected: true,
105
+ authorized: true,
106
+ provider: opts.provider,
107
+ ...(lastSendAt ? { lastSendAt } : {}),
108
+ };
109
+ },
110
+ };
111
+ }
112
+ function rejected(frame, code, message) {
113
+ return {
114
+ accepted: false,
115
+ eventId: frame.event_id ?? "",
116
+ gatewayId: frame.gateway_id ?? "",
117
+ agentId: frame.agent_id ?? "",
118
+ conversationId: frame.message?.conversation.id ?? "",
119
+ turnId: frame.event_id ? `turn_${frame.event_id}` : "turn_unknown",
120
+ error: { code, message },
121
+ };
122
+ }
@@ -3,6 +3,12 @@ import type { DaemonConfig, OpenclawGatewayProfile } from "./config.js";
3
3
  /** Per-agent metadata cached from credentials, used by `buildManagedRoutes`. */
4
4
  export interface AgentRuntimeMeta {
5
5
  runtime?: string;
6
+ /** Runtime model id/alias selected for this agent. */
7
+ runtimeModel?: string;
8
+ /** Runtime reasoning effort selected for this agent. */
9
+ reasoningEffort?: string;
10
+ /** Kimi-style thinking toggle selected for this agent. */
11
+ thinking?: boolean;
6
12
  cwd?: string;
7
13
  /** OpenClaw gateway profile name to lookup in the registry. */
8
14
  openclawGateway?: string;
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { resolveAgentIds } from "./config.js";
5
5
  import { agentWorkspaceDir } from "./agent-workspace.js";
6
6
  import { log as daemonLog } from "./log.js";
7
+ import { buildRuntimeSelectionExtraArgs, mergeRuntimeExtraArgs, } from "./runtime-route-options.js";
7
8
  function expandHome(p) {
8
9
  if (p === "~")
9
10
  return homedir();
@@ -259,10 +260,10 @@ export function buildManagedRoutes(agentIds, agentRuntimes, defaultRoute, opencl
259
260
  match: { accountId: agentId },
260
261
  runtime,
261
262
  cwd: meta.cwd || agentWorkspaceDir(agentId),
262
- // Inherit defaultRoute's extraArgs so synthesized per-agent routes
263
- // pick up operator-wide flags (e.g. `--permission-mode bypassPermissions`)
264
- // that would otherwise apply only to agents listed in `cfg.routes[]`.
265
- ...(defaultRoute.extraArgs ? { extraArgs: defaultRoute.extraArgs.slice() } : {}),
263
+ ...(() => {
264
+ const extraArgs = mergeRuntimeExtraArgs(defaultRoute.extraArgs, buildRuntimeSelectionExtraArgs(runtime, meta));
265
+ return extraArgs ? { extraArgs } : {};
266
+ })(),
266
267
  };
267
268
  if (runtime === "openclaw-acp") {
268
269
  // Per RFC §3.4: prefer credentials, fall back to defaultRoute.gateway.
package/dist/daemon.d.ts CHANGED
@@ -121,6 +121,9 @@ export interface BootBackfillResult {
121
121
  credentialPathByAgentId: Map<string, string>;
122
122
  agentRuntimes: Record<string, {
123
123
  runtime?: string;
124
+ runtimeModel?: string;
125
+ reasoningEffort?: string;
126
+ thinking?: boolean;
124
127
  cwd?: string;
125
128
  openclawGateway?: string;
126
129
  openclawAgent?: string;
package/dist/daemon.js CHANGED
@@ -14,12 +14,14 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
14
14
  import { readWorkingMemorySnapshot } from "./working-memory.js";
15
15
  import { createRoomStaticContextBuilder } from "./room-context.js";
16
16
  import { createRoomContextFetcher } from "./room-context-fetcher.js";
17
+ import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
17
18
  import { buildLoopRiskPrompt, loopRiskSessionKey, recordInboundText as recordLoopRiskInbound, recordOutboundText as recordLoopRiskOutbound, } from "./loop-risk.js";
18
19
  import { composeBotCordUserTurn } from "./turn-text.js";
19
20
  import { UserAuthManager } from "./user-auth.js";
20
21
  import { PolicyResolver } from "./gateway/policy-resolver.js";
21
22
  import { scanMention } from "./mention-scan.js";
22
23
  import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
24
+ import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
23
25
  /**
24
26
  * Default hard cap for a single runtime turn. Long-running coding/research
25
27
  * tasks routinely exceed 10 minutes, so daemon-hosted agents get a larger
@@ -233,6 +235,13 @@ export async function startDaemon(opts) {
233
235
  fetchRoomInfo: roomContextFetcher,
234
236
  log: logger,
235
237
  });
238
+ const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
239
+ credentialPathByAgentId,
240
+ ...(opts.credentialsPath ? { defaultCredentialsPath: opts.credentialsPath } : {}),
241
+ ...(opts.hubBaseUrl ? { hubBaseUrl: opts.hubBaseUrl } : {}),
242
+ limit: 20,
243
+ log: logger,
244
+ });
236
245
  const scBuilders = new Map();
237
246
  const loopRiskBuilder = (msg) => buildLoopRiskPrompt({
238
247
  sessionKey: loopRiskSessionKey({
@@ -296,13 +305,18 @@ export async function startDaemon(opts) {
296
305
  text: out.text,
297
306
  });
298
307
  };
299
- // Per-agent attention policy cache (PR3, design §4.2 / §5). Seeded from
300
- // the optional `defaultAttention` / `attentionKeywords` carried by
301
- // `provision_agent`, refreshed in-place by the `policy_updated` control
302
- // frame. PR2 will plug per-room overrides into `fetchEffective`; PR3
303
- // leaves it absent so the resolver collapses to per-agent state.
308
+ // Per-agent attention policy cache (design §4.2 / §5). It is seeded from
309
+ // `provision_agent` / `policy_updated` frames when available and falls back
310
+ // to Hub on cold misses, so daemon restarts preserve dashboard policy.
311
+ const fetchAttentionPolicy = createAttentionPolicyFetcher({
312
+ credentialPathByAgentId,
313
+ defaultCredentialsPath: opts.credentialsPath,
314
+ hubBaseUrl: opts.hubBaseUrl,
315
+ log: logger,
316
+ });
304
317
  const policyResolver = new PolicyResolver({
305
- fetchGlobal: async (_agentId) => undefined,
318
+ fetchGlobal: async (agentId) => fetchAttentionPolicy({ agentId, roomId: null }),
319
+ fetchEffective: async (agentId, roomId) => fetchAttentionPolicy({ agentId, roomId }),
306
320
  });
307
321
  // Display-name lookup for the mention text-fallback. Populated from boot
308
322
  // credentials; multi-agent daemons can reuse the same map via accountId.
@@ -374,6 +388,7 @@ export async function startDaemon(opts) {
374
388
  turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
375
389
  buildSystemContext,
376
390
  buildMemoryContext,
391
+ buildRuntimeRecoveryContext,
377
392
  onInbound,
378
393
  onOutbound,
379
394
  onRuntimeCircuitBreakerChange: pushLiveRuntimeSnapshot,
@@ -527,9 +542,19 @@ export function backfillBootAgents(agents, opts) {
527
542
  for (const a of agents) {
528
543
  if (a.credentialsFile)
529
544
  credentialPathByAgentId.set(a.agentId, a.credentialsFile);
530
- if (a.runtime || a.cwd || a.openclawGateway || a.openclawAgent || a.hermesProfile) {
545
+ if (a.runtime ||
546
+ a.runtimeModel ||
547
+ a.reasoningEffort ||
548
+ typeof a.thinking === "boolean" ||
549
+ a.cwd ||
550
+ a.openclawGateway ||
551
+ a.openclawAgent ||
552
+ a.hermesProfile) {
531
553
  agentRuntimes[a.agentId] = {
532
554
  ...(a.runtime ? { runtime: a.runtime } : {}),
555
+ ...(a.runtimeModel ? { runtimeModel: a.runtimeModel } : {}),
556
+ ...(a.reasoningEffort ? { reasoningEffort: a.reasoningEffort } : {}),
557
+ ...(typeof a.thinking === "boolean" ? { thinking: a.thinking } : {}),
533
558
  ...(a.cwd ? { cwd: a.cwd } : {}),
534
559
  ...(a.openclawGateway ? { openclawGateway: a.openclawGateway } : {}),
535
560
  ...(a.openclawAgent ? { openclawAgent: a.openclawAgent } : {}),
@@ -842,7 +842,7 @@ function normalizeBlockForHub(block, seq) {
842
842
  // Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
843
843
  // Codex: {type:"item.completed", item:{type:"agent_message", text}}
844
844
  // DeepSeek: {event:"message.delta", payload:{content}} or
845
- // {event:"item.delta", payload:{payload:{kind:"agent_message", delta}}}
845
+ // {event:"item.delta", payload:{kind:"agent_message", delta}}
846
846
  let text = "";
847
847
  const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
848
848
  for (const c of contents) {
@@ -856,9 +856,13 @@ function normalizeBlockForHub(block, seq) {
856
856
  }
857
857
  if (!text &&
858
858
  raw?.event === "item.delta" &&
859
- raw?.payload?.payload?.kind === "agent_message" &&
860
- typeof raw?.payload?.payload?.delta === "string") {
861
- text = raw.payload.payload.delta;
859
+ (raw?.payload?.kind === "agent_message" || raw?.payload?.payload?.kind === "agent_message")) {
860
+ text =
861
+ typeof raw?.payload?.delta === "string"
862
+ ? raw.payload.delta
863
+ : typeof raw?.payload?.payload?.delta === "string"
864
+ ? raw.payload.payload.delta
865
+ : "";
862
866
  }
863
867
  return { kind: "assistant", seq, payload: { text } };
864
868
  }
@@ -1048,8 +1052,12 @@ function extractDeepseekToolCall(raw) {
1048
1052
  status: stringField(payload, "status") ?? stringField(tool, "status"),
1049
1053
  };
1050
1054
  }
1051
- if (payload.event === "item.started") {
1052
- const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1055
+ if (raw?.event === "item.started" || payload.event === "item.started") {
1056
+ const inner = raw?.event === "item.started"
1057
+ ? payload
1058
+ : payload.payload && typeof payload.payload === "object"
1059
+ ? payload.payload
1060
+ : {};
1053
1061
  const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1054
1062
  const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
1055
1063
  return {
@@ -1082,8 +1090,15 @@ function extractDeepseekToolResult(raw) {
1082
1090
  id: stringField(payload, "id"),
1083
1091
  };
1084
1092
  }
1085
- if (payload.event === "item.completed" || payload.event === "item.failed") {
1086
- const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1093
+ if (raw?.event === "item.completed" ||
1094
+ raw?.event === "item.failed" ||
1095
+ payload.event === "item.completed" ||
1096
+ payload.event === "item.failed") {
1097
+ const inner = raw?.event === "item.completed" || raw?.event === "item.failed"
1098
+ ? payload
1099
+ : payload.payload && typeof payload.payload === "object"
1100
+ ? payload.payload
1101
+ : {};
1087
1102
  const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1088
1103
  const result = item?.output ??
1089
1104
  item?.result ??
@@ -1112,7 +1127,12 @@ function formatBlockDetails(raw) {
1112
1127
  : typeof r.message === "string" ? r.message
1113
1128
  : typeof r.summary === "string" ? r.summary
1114
1129
  : typeof r.label === "string" ? r.label
1115
- : "";
1130
+ : typeof r.payload?.delta === "string" ? r.payload.delta
1131
+ : typeof r.payload?.item?.detail === "string" ? r.payload.item.detail
1132
+ : typeof r.payload?.item?.summary === "string" ? r.payload.item.summary
1133
+ : typeof r.payload?.payload?.item?.detail === "string" ? r.payload.payload.item.detail
1134
+ : typeof r.payload?.payload?.item?.summary === "string" ? r.payload.payload.item.summary
1135
+ : "";
1116
1136
  if (direct)
1117
1137
  return direct;
1118
1138
  const contentText = extractContentText(r.content ?? r.message?.content ?? r.params?.update?.content);
@@ -60,6 +60,18 @@ export declare class LoginSessionStore {
60
60
  create(input: Omit<LoginSession, "expiresAt"> & {
61
61
  expiresAt?: number;
62
62
  }): LoginSession;
63
+ /**
64
+ * Distinguish whether `loginId` is unknown to the store ("missing") vs
65
+ * known-but-past-TTL ("expired"). When the entry is expired this also
66
+ * evicts it from the internal map so callers do not need to follow up
67
+ * with a separate `delete`. Use this when the caller wants to surface
68
+ * a precise error code to the user; prefer `get` when a single nullable
69
+ * result is enough.
70
+ */
71
+ resolve(loginId: string): {
72
+ state: "live" | "expired" | "missing";
73
+ session?: LoginSession;
74
+ };
63
75
  /** Get a non-expired session by id, or `null` when missing/expired. */
64
76
  get(loginId: string): LoginSession | null;
65
77
  /**
@@ -35,10 +35,28 @@ export class LoginSessionStore {
35
35
  this.sessions.set(session.loginId, session);
36
36
  return session;
37
37
  }
38
+ /**
39
+ * Distinguish whether `loginId` is unknown to the store ("missing") vs
40
+ * known-but-past-TTL ("expired"). When the entry is expired this also
41
+ * evicts it from the internal map so callers do not need to follow up
42
+ * with a separate `delete`. Use this when the caller wants to surface
43
+ * a precise error code to the user; prefer `get` when a single nullable
44
+ * result is enough.
45
+ */
46
+ resolve(loginId) {
47
+ const s = this.sessions.get(loginId);
48
+ if (!s)
49
+ return { state: "missing" };
50
+ if (s.expiresAt <= this.now()) {
51
+ this.sessions.delete(loginId);
52
+ return { state: "expired" };
53
+ }
54
+ return { state: "live", session: s };
55
+ }
38
56
  /** Get a non-expired session by id, or `null` when missing/expired. */
39
57
  get(loginId) {
40
- this.sweep();
41
- return this.sessions.get(loginId) ?? null;
58
+ const { state, session } = this.resolve(loginId);
59
+ return state === "live" && session ? session : null;
42
60
  }
43
61
  /**
44
62
  * Apply a partial patch to the session in place. No-op when the session
@@ -1,20 +1,7 @@
1
1
  /**
2
- * Sanitize untrusted inbound content before handing it off to a local runtime.
3
- *
4
- * Copied from `packages/daemon/src/sanitize.ts` so the gateway channel adapter
5
- * does not depend back on the daemon package. Keep these two files in sync —
6
- * any new structural marker added in one place should be mirrored in the other.
7
- *
8
- * Neutralizes:
9
- * - BotCord structural markers the channel itself emits (so peers can't forge them).
10
- * - Common LLM prompt-injection patterns (<system>, [INST], <<SYS>>, <|im_start|>, etc.).
11
- * - Wrapper XML tags the channel uses to frame inbound content
12
- * (<agent-message>, <human-message>, <room-rule>).
2
+ * Thin re-export `sanitizeUntrustedContent` / `sanitizeSenderName` live
3
+ * in `@botcord/protocol-core` so the daemon channel adapters and the
4
+ * `gateway-ingress` provider adapters use one canonical implementation.
5
+ * Existing imports of this module keep working unchanged.
13
6
  */
14
- export declare function sanitizeUntrustedContent(text: string): string;
15
- /**
16
- * Sanitize a sender label so it's safe to embed inside
17
- * `<agent-message sender="...">`. Must not contain newlines, structural
18
- * markers, or characters that could break the XML attribute boundary.
19
- */
20
- export declare function sanitizeSenderName(name: string): string;
7
+ export { sanitizeUntrustedContent, sanitizeSenderName, } from "@botcord/protocol-core";
@@ -1,56 +1,7 @@
1
1
  /**
2
- * Sanitize untrusted inbound content before handing it off to a local runtime.
3
- *
4
- * Copied from `packages/daemon/src/sanitize.ts` so the gateway channel adapter
5
- * does not depend back on the daemon package. Keep these two files in sync —
6
- * any new structural marker added in one place should be mirrored in the other.
7
- *
8
- * Neutralizes:
9
- * - BotCord structural markers the channel itself emits (so peers can't forge them).
10
- * - Common LLM prompt-injection patterns (<system>, [INST], <<SYS>>, <|im_start|>, etc.).
11
- * - Wrapper XML tags the channel uses to frame inbound content
12
- * (<agent-message>, <human-message>, <room-rule>).
2
+ * Thin re-export `sanitizeUntrustedContent` / `sanitizeSenderName` live
3
+ * in `@botcord/protocol-core` so the daemon channel adapters and the
4
+ * `gateway-ingress` provider adapters use one canonical implementation.
5
+ * Existing imports of this module keep working unchanged.
13
6
  */
14
- export function sanitizeUntrustedContent(text) {
15
- let s = text;
16
- s = s.replace(/<\/?a[\s]*g[\s]*e[\s]*n[\s]*t[\s]*-[\s]*m[\s]*e[\s]*s[\s]*s[\s]*a[\s]*g[\s]*e[\s\S]*?>/gi, "[⚠ stripped: agent-message tag]");
17
- s = s.replace(/<\/?h[\s]*u[\s]*m[\s]*a[\s]*n[\s]*-[\s]*m[\s]*e[\s]*s[\s]*s[\s]*a[\s]*g[\s]*e[\s\S]*?>/gi, "[⚠ stripped: human-message tag]");
18
- s = s.replace(/<\/?r[\s]*o[\s]*o[\s]*m[\s]*-[\s]*r[\s]*u[\s]*l[\s]*e[\s\S]*?>/gi, "[⚠ stripped: room-rule tag]");
19
- return s
20
- .split(/\r?\n/)
21
- .map((line) => {
22
- let l = line;
23
- l = l.replace(/^\[(BotCord (?:Message|Notification))\]/i, "[⚠ fake: $1]");
24
- l = l.replace(/^\[Room Rule\]/i, "[⚠ fake: Room Rule]");
25
- l = l.replace(/^\[房间规则\]/i, "[⚠ fake: 房间规则]");
26
- l = l.replace(/^\[系统提示\]/i, "[⚠ fake: 系统提示]");
27
- l = l.replace(/^\[BotCord\s+([^\]\r\n]+)\]/i, (_m, label) => {
28
- const head = String(label).split(":")[0].trim() || String(label).trim();
29
- return `[⚠ fake: BotCord ${head}]`;
30
- });
31
- l = l.replace(/^\[(System|SYSTEM|Assistant|ASSISTANT|User|USER)\]/, "[⚠ fake: $1]");
32
- l = l.replace(/<\/?\s*system(?:-reminder)?\s*>/gi, "[⚠ stripped: system tag]");
33
- l = l.replace(/<\|im_start\|>/gi, "[⚠ stripped: im_start]");
34
- l = l.replace(/<\|im_end\|>/gi, "[⚠ stripped: im_end]");
35
- l = l.replace(/\[\/?INST\]/gi, "[⚠ stripped: INST]");
36
- l = l.replace(/<<\/?SYS>>/gi, "[⚠ stripped: SYS]");
37
- l = l.replace(/<\s*\/?\|(?:system|user|assistant)\|?\s*>/gi, "[⚠ stripped: role tag]");
38
- return l;
39
- })
40
- .join("\n");
41
- }
42
- /**
43
- * Sanitize a sender label so it's safe to embed inside
44
- * `<agent-message sender="...">`. Must not contain newlines, structural
45
- * markers, or characters that could break the XML attribute boundary.
46
- */
47
- export function sanitizeSenderName(name) {
48
- return name
49
- .replace(/[\n\r]/g, " ")
50
- .replace(/\[/g, "⟦")
51
- .replace(/\]/g, "⟧")
52
- .replace(/"/g, "'")
53
- .replace(/</g, "<")
54
- .replace(/>/g, ">")
55
- .slice(0, 100);
56
- }
7
+ export { sanitizeUntrustedContent, sanitizeSenderName, } from "@botcord/protocol-core";
@@ -1,13 +1,7 @@
1
1
  /**
2
- * Split a long message into chunks <= `limit` characters each. Prefers to cut
3
- * on newline boundaries so multi-paragraph replies don't fragment mid-line.
4
- *
5
- * Shared by third-party channel adapters (Telegram, WeChat) which both have a
6
- * per-message size cap from upstream and no native streaming. WeChat caller
7
- * passes a smaller `limit` (~1800), Telegram a larger one (~4000, since the
8
- * raw Telegram limit is 4096).
9
- *
10
- * Empty input returns `[""]` so callers can iterate uniformly without a length
11
- * check.
2
+ * Thin re-export `splitText` lives in `@botcord/protocol-core` so the
3
+ * daemon channel adapters and the `gateway-ingress` provider adapters use
4
+ * one canonical implementation. Existing imports of this module keep
5
+ * working unchanged.
12
6
  */
13
- export declare function splitText(text: string, limit: number): string[];
7
+ export { splitText } from "@botcord/protocol-core";