@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.
- package/dist/agent-discovery.d.ts +6 -0
- package/dist/agent-discovery.js +6 -0
- package/dist/attention-policy-fetcher.d.ts +14 -0
- package/dist/attention-policy-fetcher.js +59 -0
- package/dist/cloud-daemon.js +8 -0
- package/dist/cloud-gateway-runtime.d.ts +29 -0
- package/dist/cloud-gateway-runtime.js +122 -0
- package/dist/daemon-config-map.d.ts +6 -0
- package/dist/daemon-config-map.js +5 -4
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +32 -7
- package/dist/gateway/channels/botcord.js +29 -9
- package/dist/gateway/channels/login-session.d.ts +12 -0
- package/dist/gateway/channels/login-session.js +20 -2
- package/dist/gateway/channels/sanitize.d.ts +5 -18
- package/dist/gateway/channels/sanitize.js +5 -54
- package/dist/gateway/channels/text-split.d.ts +5 -11
- package/dist/gateway/channels/text-split.js +5 -31
- package/dist/gateway/dispatcher.d.ts +7 -1
- package/dist/gateway/dispatcher.js +88 -8
- package/dist/gateway/gateway.d.ts +16 -1
- package/dist/gateway/gateway.js +21 -0
- package/dist/gateway/policy-resolver.js +17 -9
- package/dist/gateway/runtimes/deepseek-tui.js +86 -19
- package/dist/gateway/types.d.ts +12 -57
- package/dist/gateway-control.js +18 -9
- package/dist/provision.d.ts +9 -3
- package/dist/provision.js +181 -9
- package/dist/room-recovery-context.d.ts +11 -0
- package/dist/room-recovery-context.js +97 -0
- package/dist/runtime-models.d.ts +17 -0
- package/dist/runtime-models.js +953 -0
- package/dist/runtime-route-options.d.ts +7 -0
- package/dist/runtime-route-options.js +45 -0
- package/package.json +2 -2
- package/src/__tests__/attention-policy-fetcher.test.ts +67 -0
- package/src/__tests__/cloud-gateway-runtime.test.ts +127 -0
- package/src/__tests__/daemon-config-map.test.ts +26 -1
- package/src/__tests__/gateway-control.test.ts +136 -0
- package/src/__tests__/policy-resolver.test.ts +20 -0
- package/src/__tests__/provision.test.ts +124 -0
- package/src/__tests__/runtime-discovery.test.ts +68 -9
- package/src/__tests__/runtime-models.test.ts +333 -0
- package/src/agent-discovery.ts +9 -0
- package/src/attention-policy-fetcher.ts +87 -0
- package/src/cloud-daemon.ts +8 -0
- package/src/cloud-gateway-runtime.ts +171 -0
- package/src/daemon-config-map.ts +17 -4
- package/src/daemon.ts +38 -9
- package/src/gateway/__tests__/botcord-channel.test.ts +97 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +207 -1
- package/src/gateway/__tests__/dispatcher.test.ts +56 -0
- package/src/gateway/channels/botcord.ts +32 -8
- package/src/gateway/channels/login-session.ts +20 -2
- package/src/gateway/channels/sanitize.ts +8 -66
- package/src/gateway/channels/text-split.ts +5 -27
- package/src/gateway/dispatcher.ts +123 -27
- package/src/gateway/gateway.ts +29 -0
- package/src/gateway/policy-resolver.ts +20 -9
- package/src/gateway/runtimes/deepseek-tui.ts +86 -19
- package/src/gateway/types.ts +31 -59
- package/src/gateway-control.ts +21 -9
- package/src/provision.ts +202 -11
- package/src/room-recovery-context.ts +131 -0
- package/src/runtime-models.ts +972 -0
- 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). */
|
package/dist/agent-discovery.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/cloud-daemon.js
CHANGED
|
@@ -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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 (
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
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 (
|
|
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 ||
|
|
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:{
|
|
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
|
-
|
|
861
|
-
|
|
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 =
|
|
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 (
|
|
1086
|
-
|
|
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.
|
|
41
|
-
return
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
7
|
+
export { splitText } from "@botcord/protocol-core";
|