@botcord/daemon 0.2.78 → 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/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.js +21 -6
- 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 +31 -12
- package/dist/gateway/types.d.ts +12 -57
- package/dist/gateway-control.js +18 -9
- package/dist/provision.d.ts +7 -3
- package/dist/provision.js +115 -8
- package/dist/room-recovery-context.d.ts +11 -0
- package/dist/room-recovery-context.js +97 -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__/gateway-control.test.ts +136 -0
- package/src/__tests__/policy-resolver.test.ts +20 -0
- package/src/__tests__/provision.test.ts +65 -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.ts +23 -6
- package/src/gateway/__tests__/botcord-channel.test.ts +97 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +177 -0
- 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 +37 -12
- package/src/gateway/types.ts +31 -59
- package/src/gateway-control.ts +21 -9
- package/src/provision.ts +133 -7
- package/src/room-recovery-context.ts +131 -0
|
@@ -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
|
+
}
|
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,
|
|
@@ -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";
|
|
@@ -1,33 +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
|
|
14
|
-
if (limit <= 0)
|
|
15
|
-
return [text];
|
|
16
|
-
if (text.length === 0)
|
|
17
|
-
return [""];
|
|
18
|
-
if (text.length <= limit)
|
|
19
|
-
return [text];
|
|
20
|
-
const out = [];
|
|
21
|
-
let remaining = text;
|
|
22
|
-
while (remaining.length > limit) {
|
|
23
|
-
let cut = remaining.lastIndexOf("\n", limit);
|
|
24
|
-
if (cut <= 0)
|
|
25
|
-
cut = limit;
|
|
26
|
-
out.push(remaining.slice(0, cut));
|
|
27
|
-
// Drop the leading newline so the next chunk doesn't start with a blank line.
|
|
28
|
-
remaining = remaining.slice(cut).replace(/^\n/, "");
|
|
29
|
-
}
|
|
30
|
-
if (remaining.length > 0)
|
|
31
|
-
out.push(remaining);
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
7
|
+
export { splitText } from "@botcord/protocol-core";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GatewayLogger } from "./log.js";
|
|
2
2
|
import { type SessionStore } from "./session-store.js";
|
|
3
3
|
import { type TranscriptWriter } from "./transcript.js";
|
|
4
|
-
import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, RuntimeRunResult, RuntimeCircuitBreakerSnapshot, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
|
|
4
|
+
import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, RuntimeRecoveryContextBuilder, RuntimeRunResult, RuntimeCircuitBreakerSnapshot, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
|
|
5
5
|
/** Factory signature for building a runtime adapter at turn dispatch time. */
|
|
6
6
|
export type RuntimeFactory = (runtimeId: string, extraArgs?: string[]) => RuntimeAdapter;
|
|
7
7
|
/** Constructor options for `Dispatcher`. */
|
|
@@ -33,6 +33,11 @@ export interface DispatcherOptions {
|
|
|
33
33
|
* keep following stale memory.
|
|
34
34
|
*/
|
|
35
35
|
buildMemoryContext?: MemoryContextBuilder;
|
|
36
|
+
/**
|
|
37
|
+
* Optional hook that returns recent room context for a fresh-session retry
|
|
38
|
+
* after a runtime resume session becomes unrecoverable.
|
|
39
|
+
*/
|
|
40
|
+
buildRuntimeRecoveryContext?: RuntimeRecoveryContextBuilder;
|
|
36
41
|
/**
|
|
37
42
|
* Optional side-effect hook invoked after ack, before the turn runs.
|
|
38
43
|
* Intended for bookkeeping (e.g. activity tracking). Errors are logged
|
|
@@ -116,6 +121,7 @@ export declare class Dispatcher {
|
|
|
116
121
|
private readonly runtimeAuthFailureCooldownMs;
|
|
117
122
|
private readonly buildSystemContext?;
|
|
118
123
|
private readonly buildMemoryContext?;
|
|
124
|
+
private readonly buildRuntimeRecoveryContext?;
|
|
119
125
|
private readonly onInbound?;
|
|
120
126
|
private readonly onOutbound?;
|
|
121
127
|
private readonly onTurnComplete?;
|