@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
package/src/daemon.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
|
39
39
|
import { readWorkingMemorySnapshot } from "./working-memory.js";
|
|
40
40
|
import { createRoomStaticContextBuilder } from "./room-context.js";
|
|
41
41
|
import { createRoomContextFetcher } from "./room-context-fetcher.js";
|
|
42
|
+
import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
|
|
42
43
|
import {
|
|
43
44
|
buildLoopRiskPrompt,
|
|
44
45
|
loopRiskSessionKey,
|
|
@@ -50,6 +51,7 @@ import { UserAuthManager } from "./user-auth.js";
|
|
|
50
51
|
import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
|
|
51
52
|
import { scanMention } from "./mention-scan.js";
|
|
52
53
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
54
|
+
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
@@ -364,6 +366,13 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
364
366
|
fetchRoomInfo: roomContextFetcher,
|
|
365
367
|
log: logger,
|
|
366
368
|
});
|
|
369
|
+
const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
|
|
370
|
+
credentialPathByAgentId,
|
|
371
|
+
...(opts.credentialsPath ? { defaultCredentialsPath: opts.credentialsPath } : {}),
|
|
372
|
+
...(opts.hubBaseUrl ? { hubBaseUrl: opts.hubBaseUrl } : {}),
|
|
373
|
+
limit: 20,
|
|
374
|
+
log: logger,
|
|
375
|
+
});
|
|
367
376
|
|
|
368
377
|
// Cache one system-context builder per configured agentId. The gateway
|
|
369
378
|
// calls this with each inbound message and we pick the right builder by
|
|
@@ -442,13 +451,20 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
442
451
|
});
|
|
443
452
|
};
|
|
444
453
|
|
|
445
|
-
// Per-agent attention policy cache (
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
454
|
+
// Per-agent attention policy cache (design §4.2 / §5). It is seeded from
|
|
455
|
+
// `provision_agent` / `policy_updated` frames when available and falls back
|
|
456
|
+
// to Hub on cold misses, so daemon restarts preserve dashboard policy.
|
|
457
|
+
const fetchAttentionPolicy = createAttentionPolicyFetcher({
|
|
458
|
+
credentialPathByAgentId,
|
|
459
|
+
defaultCredentialsPath: opts.credentialsPath,
|
|
460
|
+
hubBaseUrl: opts.hubBaseUrl,
|
|
461
|
+
log: logger,
|
|
462
|
+
});
|
|
450
463
|
const policyResolver = new PolicyResolver({
|
|
451
|
-
fetchGlobal: async (
|
|
464
|
+
fetchGlobal: async (agentId: string) =>
|
|
465
|
+
fetchAttentionPolicy({ agentId, roomId: null }),
|
|
466
|
+
fetchEffective: async (agentId: string, roomId: string) =>
|
|
467
|
+
fetchAttentionPolicy({ agentId, roomId }),
|
|
452
468
|
});
|
|
453
469
|
|
|
454
470
|
// Display-name lookup for the mention text-fallback. Populated from boot
|
|
@@ -528,6 +544,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
528
544
|
turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
|
|
529
545
|
buildSystemContext,
|
|
530
546
|
buildMemoryContext,
|
|
547
|
+
buildRuntimeRecoveryContext,
|
|
531
548
|
onInbound,
|
|
532
549
|
onOutbound,
|
|
533
550
|
onRuntimeCircuitBreakerChange: pushLiveRuntimeSnapshot,
|
|
@@ -684,7 +701,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
684
701
|
*/
|
|
685
702
|
export interface BootBackfillResult {
|
|
686
703
|
credentialPathByAgentId: Map<string, string>;
|
|
687
|
-
agentRuntimes: Record<string, { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }>;
|
|
704
|
+
agentRuntimes: Record<string, { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }>;
|
|
688
705
|
}
|
|
689
706
|
|
|
690
707
|
/**
|
|
@@ -703,13 +720,25 @@ export function backfillBootAgents(
|
|
|
703
720
|
): BootBackfillResult {
|
|
704
721
|
const ensure = opts.ensure ?? ensureAgentWorkspace;
|
|
705
722
|
const credentialPathByAgentId = new Map<string, string>();
|
|
706
|
-
const agentRuntimes: Record<string, { runtime?: string; cwd?: string }> = {};
|
|
723
|
+
const agentRuntimes: Record<string, { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }> = {};
|
|
707
724
|
const failed: string[] = [];
|
|
708
725
|
for (const a of agents) {
|
|
709
726
|
if (a.credentialsFile) credentialPathByAgentId.set(a.agentId, a.credentialsFile);
|
|
710
|
-
if (
|
|
727
|
+
if (
|
|
728
|
+
a.runtime ||
|
|
729
|
+
a.runtimeModel ||
|
|
730
|
+
a.reasoningEffort ||
|
|
731
|
+
typeof a.thinking === "boolean" ||
|
|
732
|
+
a.cwd ||
|
|
733
|
+
a.openclawGateway ||
|
|
734
|
+
a.openclawAgent ||
|
|
735
|
+
a.hermesProfile
|
|
736
|
+
) {
|
|
711
737
|
agentRuntimes[a.agentId] = {
|
|
712
738
|
...(a.runtime ? { runtime: a.runtime } : {}),
|
|
739
|
+
...(a.runtimeModel ? { runtimeModel: a.runtimeModel } : {}),
|
|
740
|
+
...(a.reasoningEffort ? { reasoningEffort: a.reasoningEffort } : {}),
|
|
741
|
+
...(typeof a.thinking === "boolean" ? { thinking: a.thinking } : {}),
|
|
713
742
|
...(a.cwd ? { cwd: a.cwd } : {}),
|
|
714
743
|
...(a.openclawGateway ? { openclawGateway: a.openclawGateway } : {}),
|
|
715
744
|
...(a.openclawAgent ? { openclawAgent: a.openclawAgent } : {}),
|
|
@@ -925,6 +925,103 @@ describe("createBotCordChannel — streamBlock()", () => {
|
|
|
925
925
|
}
|
|
926
926
|
});
|
|
927
927
|
|
|
928
|
+
it("normalizes current DeepSeek item.delta assistant text", async () => {
|
|
929
|
+
const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
930
|
+
const realFetch = globalThis.fetch;
|
|
931
|
+
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
|
932
|
+
try {
|
|
933
|
+
const client = makeClient({
|
|
934
|
+
getHubUrl: vi.fn().mockReturnValue("https://hub.example.com"),
|
|
935
|
+
});
|
|
936
|
+
const channel = createBotCordChannel({
|
|
937
|
+
id: "botcord-main",
|
|
938
|
+
accountId: "ag_self",
|
|
939
|
+
agentId: "ag_self",
|
|
940
|
+
client,
|
|
941
|
+
hubBaseUrl: "https://hub.example.com",
|
|
942
|
+
});
|
|
943
|
+
await channel.streamBlock!({
|
|
944
|
+
traceId: "m_trace",
|
|
945
|
+
accountId: "ag_self",
|
|
946
|
+
conversationId: "rm_oc_1",
|
|
947
|
+
block: {
|
|
948
|
+
kind: "assistant_text",
|
|
949
|
+
seq: 6,
|
|
950
|
+
raw: {
|
|
951
|
+
event: "item.delta",
|
|
952
|
+
payload: { thread_id: "thr_1", turn_id: "turn_1", kind: "agent_message", delta: "hello" },
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
log: silentLog,
|
|
956
|
+
});
|
|
957
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
958
|
+
const body = JSON.parse(init.body as string);
|
|
959
|
+
expect(body.block).toEqual({
|
|
960
|
+
kind: "assistant",
|
|
961
|
+
seq: 6,
|
|
962
|
+
payload: { text: "hello" },
|
|
963
|
+
});
|
|
964
|
+
} finally {
|
|
965
|
+
globalThis.fetch = realFetch;
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("normalizes current DeepSeek item.started tool input", () => {
|
|
970
|
+
expect(
|
|
971
|
+
__normalizeBlockForHubForTests(
|
|
972
|
+
{
|
|
973
|
+
kind: "tool_use",
|
|
974
|
+
seq: 7,
|
|
975
|
+
raw: {
|
|
976
|
+
event: "item.started",
|
|
977
|
+
payload: {
|
|
978
|
+
item: { id: "item_tool", kind: "tool_call", status: "in_progress" },
|
|
979
|
+
tool: { id: "call_1", name: "web_search", input: { query: "上海天气" } },
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
7,
|
|
984
|
+
),
|
|
985
|
+
).toEqual({
|
|
986
|
+
kind: "tool_call",
|
|
987
|
+
seq: 7,
|
|
988
|
+
payload: {
|
|
989
|
+
id: "call_1",
|
|
990
|
+
name: "web_search",
|
|
991
|
+
params: { query: "上海天气" },
|
|
992
|
+
status: "in_progress",
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it("normalizes current DeepSeek agent_reasoning details", () => {
|
|
998
|
+
expect(
|
|
999
|
+
__normalizeBlockForHubForTests(
|
|
1000
|
+
{
|
|
1001
|
+
kind: "thinking",
|
|
1002
|
+
seq: 8,
|
|
1003
|
+
raw: {
|
|
1004
|
+
event: "item.completed",
|
|
1005
|
+
payload: {
|
|
1006
|
+
item: {
|
|
1007
|
+
id: "item_reasoning",
|
|
1008
|
+
kind: "agent_reasoning",
|
|
1009
|
+
status: "completed",
|
|
1010
|
+
summary: "I should answer briefly.",
|
|
1011
|
+
detail: "I should answer briefly.",
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
8,
|
|
1017
|
+
),
|
|
1018
|
+
).toEqual({
|
|
1019
|
+
kind: "thinking",
|
|
1020
|
+
seq: 8,
|
|
1021
|
+
payload: { details: "I should answer briefly." },
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
928
1025
|
it("marks DeepSeek terminal events for owner-chat stream cleanup", async () => {
|
|
929
1026
|
const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
930
1027
|
const realFetch = globalThis.fetch;
|
|
@@ -106,7 +106,12 @@ async function startMockDeepseekServer(opts?: {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function runAdapter(
|
|
109
|
+
function runAdapter(
|
|
110
|
+
serverUrl: string,
|
|
111
|
+
authToken: string,
|
|
112
|
+
sessionId: string | null = null,
|
|
113
|
+
extraArgs?: string[],
|
|
114
|
+
) {
|
|
110
115
|
const adapter = new DeepseekTuiAdapter({ serverUrl, authToken });
|
|
111
116
|
const ctrl = new AbortController();
|
|
112
117
|
const blocks: string[] = [];
|
|
@@ -118,6 +123,7 @@ function runAdapter(serverUrl: string, authToken: string, sessionId: string | nu
|
|
|
118
123
|
cwd: tmpRoot,
|
|
119
124
|
signal: ctrl.signal,
|
|
120
125
|
trustLevel: "owner",
|
|
126
|
+
extraArgs,
|
|
121
127
|
systemContext: "runtime memory",
|
|
122
128
|
onBlock: (b) => blocks.push(b.kind),
|
|
123
129
|
onStatus: (e) => {
|
|
@@ -169,6 +175,183 @@ describe("DeepseekTuiAdapter", () => {
|
|
|
169
175
|
}
|
|
170
176
|
});
|
|
171
177
|
|
|
178
|
+
it("parses current DeepSeek item.delta agent_message events as assistant text", async () => {
|
|
179
|
+
const server = await startMockDeepseekServer({
|
|
180
|
+
events: [
|
|
181
|
+
{
|
|
182
|
+
event: "turn.started",
|
|
183
|
+
data: {
|
|
184
|
+
seq: 1,
|
|
185
|
+
thread_id: "thr_test",
|
|
186
|
+
turn_id: "turn_test",
|
|
187
|
+
event: "turn.started",
|
|
188
|
+
payload: { turn: { status: "in_progress" } },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
event: "item.started",
|
|
193
|
+
data: {
|
|
194
|
+
seq: 2,
|
|
195
|
+
thread_id: "thr_test",
|
|
196
|
+
turn_id: "turn_test",
|
|
197
|
+
item_id: "item_msg",
|
|
198
|
+
event: "item.started",
|
|
199
|
+
payload: { item: { id: "item_msg", kind: "agent_message", status: "in_progress" } },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
event: "item.delta",
|
|
204
|
+
data: {
|
|
205
|
+
seq: 3,
|
|
206
|
+
thread_id: "thr_test",
|
|
207
|
+
turn_id: "turn_test",
|
|
208
|
+
item_id: "item_msg",
|
|
209
|
+
event: "item.delta",
|
|
210
|
+
payload: { kind: "agent_message", delta: "hello " },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
event: "item.delta",
|
|
215
|
+
data: {
|
|
216
|
+
seq: 4,
|
|
217
|
+
thread_id: "thr_test",
|
|
218
|
+
turn_id: "turn_test",
|
|
219
|
+
item_id: "item_msg",
|
|
220
|
+
event: "item.delta",
|
|
221
|
+
payload: { kind: "agent_message", delta: "deepseek" },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
event: "turn.completed",
|
|
226
|
+
data: {
|
|
227
|
+
seq: 5,
|
|
228
|
+
thread_id: "thr_test",
|
|
229
|
+
turn_id: "turn_test",
|
|
230
|
+
event: "turn.completed",
|
|
231
|
+
payload: { turn: { status: "completed" } },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
const { result, blocks, status } = runAdapter(server.baseUrl, server.token);
|
|
238
|
+
const res = await result;
|
|
239
|
+
expect(res).toEqual({ text: "hello deepseek", newSessionId: server.threadId });
|
|
240
|
+
expect(blocks).toContain("assistant_text");
|
|
241
|
+
expect(status.at(-1)).toEqual({ phase: "stopped", label: undefined });
|
|
242
|
+
} finally {
|
|
243
|
+
await server.close();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("parses current DeepSeek item.started/item.completed tool events", async () => {
|
|
248
|
+
const server = await startMockDeepseekServer({
|
|
249
|
+
events: [
|
|
250
|
+
{
|
|
251
|
+
event: "turn.started",
|
|
252
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.started" },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
event: "item.started",
|
|
256
|
+
data: {
|
|
257
|
+
thread_id: "thr_test",
|
|
258
|
+
turn_id: "turn_test",
|
|
259
|
+
event: "item.started",
|
|
260
|
+
payload: {
|
|
261
|
+
item: { id: "item_tool", kind: "tool_call", status: "in_progress" },
|
|
262
|
+
tool: { id: "call_1", name: "web_search", input: { query: "Shanghai weather" } },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
event: "item.completed",
|
|
268
|
+
data: {
|
|
269
|
+
thread_id: "thr_test",
|
|
270
|
+
turn_id: "turn_test",
|
|
271
|
+
event: "item.completed",
|
|
272
|
+
payload: {
|
|
273
|
+
item: {
|
|
274
|
+
id: "item_tool",
|
|
275
|
+
kind: "tool_call",
|
|
276
|
+
status: "completed",
|
|
277
|
+
detail: "Found 5 result(s)",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
event: "item.delta",
|
|
284
|
+
data: {
|
|
285
|
+
thread_id: "thr_test",
|
|
286
|
+
turn_id: "turn_test",
|
|
287
|
+
event: "item.delta",
|
|
288
|
+
payload: { kind: "agent_message", delta: "done" },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
event: "turn.completed",
|
|
293
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.completed" },
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
const { result, blocks, status } = runAdapter(server.baseUrl, server.token);
|
|
299
|
+
await expect(result).resolves.toMatchObject({ text: "done" });
|
|
300
|
+
expect(blocks).toEqual(expect.arrayContaining(["tool_use", "tool_result", "assistant_text"]));
|
|
301
|
+
expect(status).toContainEqual({ phase: "updated", label: "web_search" });
|
|
302
|
+
} finally {
|
|
303
|
+
await server.close();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("emits current DeepSeek agent_reasoning completions as thinking blocks", async () => {
|
|
308
|
+
const server = await startMockDeepseekServer({
|
|
309
|
+
events: [
|
|
310
|
+
{
|
|
311
|
+
event: "turn.started",
|
|
312
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.started" },
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
event: "item.completed",
|
|
316
|
+
data: {
|
|
317
|
+
thread_id: "thr_test",
|
|
318
|
+
turn_id: "turn_test",
|
|
319
|
+
event: "item.completed",
|
|
320
|
+
payload: {
|
|
321
|
+
item: {
|
|
322
|
+
id: "item_reasoning",
|
|
323
|
+
kind: "agent_reasoning",
|
|
324
|
+
status: "completed",
|
|
325
|
+
summary: "I should answer briefly.",
|
|
326
|
+
detail: "I should answer briefly.",
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
event: "item.delta",
|
|
333
|
+
data: {
|
|
334
|
+
thread_id: "thr_test",
|
|
335
|
+
turn_id: "turn_test",
|
|
336
|
+
event: "item.delta",
|
|
337
|
+
payload: { kind: "agent_message", delta: "hi" },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
event: "turn.completed",
|
|
342
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.completed" },
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
const { result, blocks } = runAdapter(server.baseUrl, server.token);
|
|
348
|
+
await expect(result).resolves.toMatchObject({ text: "hi" });
|
|
349
|
+
expect(blocks).toContain("thinking");
|
|
350
|
+
} finally {
|
|
351
|
+
await server.close();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
172
355
|
it("reuses an existing DeepSeek thread id and patches per-turn system context", async () => {
|
|
173
356
|
const server = await startMockDeepseekServer({ threadId: "thr_existing" });
|
|
174
357
|
try {
|
|
@@ -184,6 +367,29 @@ describe("DeepseekTuiAdapter", () => {
|
|
|
184
367
|
}
|
|
185
368
|
});
|
|
186
369
|
|
|
370
|
+
it("passes selected model and reasoning effort through HTTP payloads", async () => {
|
|
371
|
+
const server = await startMockDeepseekServer();
|
|
372
|
+
try {
|
|
373
|
+
const { result } = runAdapter(server.baseUrl, server.token, null, [
|
|
374
|
+
"--model",
|
|
375
|
+
"deepseek-v4-pro",
|
|
376
|
+
"--reasoning-effort",
|
|
377
|
+
"auto",
|
|
378
|
+
]);
|
|
379
|
+
await result;
|
|
380
|
+
expect(server.calls.find((c) => c.method === "POST" && c.url === "/v1/threads")?.body).toMatchObject({
|
|
381
|
+
model: "deepseek-v4-pro",
|
|
382
|
+
reasoning_effort: "auto",
|
|
383
|
+
});
|
|
384
|
+
expect(server.calls.find((c) => c.method === "POST" && c.url.endsWith("/turns"))?.body).toMatchObject({
|
|
385
|
+
model: "deepseek-v4-pro",
|
|
386
|
+
reasoning_effort: "auto",
|
|
387
|
+
});
|
|
388
|
+
} finally {
|
|
389
|
+
await server.close();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
187
393
|
it("clears stale session ids when DeepSeek reports the thread missing", async () => {
|
|
188
394
|
const server = await startMockDeepseekServer({ threadId: "thr_other" });
|
|
189
395
|
try {
|
|
@@ -244,6 +244,7 @@ describe("Dispatcher", () => {
|
|
|
244
244
|
turnTimeoutMs?: number;
|
|
245
245
|
runtimeAuthFailureThreshold?: number;
|
|
246
246
|
runtimeAuthFailureCooldownMs?: number;
|
|
247
|
+
buildRuntimeRecoveryContext?: (message: GatewayInboundMessage) => Promise<string | null> | string | null;
|
|
247
248
|
}) {
|
|
248
249
|
const { store, dir } = await makeStore();
|
|
249
250
|
tempDirs.push(dir);
|
|
@@ -258,6 +259,7 @@ describe("Dispatcher", () => {
|
|
|
258
259
|
turnTimeoutMs: args.turnTimeoutMs,
|
|
259
260
|
runtimeAuthFailureThreshold: args.runtimeAuthFailureThreshold,
|
|
260
261
|
runtimeAuthFailureCooldownMs: args.runtimeAuthFailureCooldownMs,
|
|
262
|
+
buildRuntimeRecoveryContext: args.buildRuntimeRecoveryContext,
|
|
261
263
|
});
|
|
262
264
|
return { dispatcher, channel, store };
|
|
263
265
|
}
|
|
@@ -460,6 +462,60 @@ describe("Dispatcher", () => {
|
|
|
460
462
|
expect(channel.sends[0].message.type).toBe("error");
|
|
461
463
|
});
|
|
462
464
|
|
|
465
|
+
it("codex: retries a poisoned resumed session in a fresh session with recent room context", async () => {
|
|
466
|
+
let factoryCall = 0;
|
|
467
|
+
const recoveryRuntime: RuntimeAdapter = {
|
|
468
|
+
id: "codex",
|
|
469
|
+
run: vi.fn(async (opts: RuntimeRunOptions): Promise<RuntimeRunResult> => {
|
|
470
|
+
if ((recoveryRuntime.run as any).mock.calls.length === 1) {
|
|
471
|
+
expect(opts.sessionId).toBe("sid-1");
|
|
472
|
+
return {
|
|
473
|
+
text: "",
|
|
474
|
+
newSessionId: "sid-1",
|
|
475
|
+
error: "Codex context compaction failed: maximum context length exceeded",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
expect(opts.sessionId).toBe(null);
|
|
479
|
+
expect(opts.text).toContain("[BotCord Runtime Recovery Notice]");
|
|
480
|
+
expect(opts.text).toContain("[Recent Room Messages]");
|
|
481
|
+
expect(opts.text).toContain("Alice: deploy is failing");
|
|
482
|
+
expect(opts.text).toContain("[Current User Turn]");
|
|
483
|
+
expect(opts.text).toContain("continue");
|
|
484
|
+
return { text: "recovered", newSessionId: "sid-2" };
|
|
485
|
+
}) as RuntimeAdapter["run"],
|
|
486
|
+
};
|
|
487
|
+
const runtimeFactory: RuntimeFactory = () => {
|
|
488
|
+
factoryCall += 1;
|
|
489
|
+
if (factoryCall === 1) {
|
|
490
|
+
return new FakeRuntime({ id: "codex", reply: "ok", newSessionId: "sid-1" });
|
|
491
|
+
}
|
|
492
|
+
return recoveryRuntime;
|
|
493
|
+
};
|
|
494
|
+
const { dispatcher, store, channel } = await scaffold({
|
|
495
|
+
config: baseConfig({ defaultRoute: { runtime: "codex", cwd: "/tmp/default" } }),
|
|
496
|
+
runtimeFactory,
|
|
497
|
+
buildRuntimeRecoveryContext: () =>
|
|
498
|
+
"[Recent Room Messages]\n- Alice: deploy is failing\n- Bot: I am checking logs",
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
await dispatcher.handle(
|
|
502
|
+
makeEnvelope({ id: "msg_1", conversation: { id: "rm_oc_recover", kind: "direct" } }),
|
|
503
|
+
);
|
|
504
|
+
expect(store.all()[0].runtimeSessionId).toBe("sid-1");
|
|
505
|
+
|
|
506
|
+
await dispatcher.handle(
|
|
507
|
+
makeEnvelope({
|
|
508
|
+
id: "msg_2",
|
|
509
|
+
text: "continue",
|
|
510
|
+
conversation: { id: "rm_oc_recover", kind: "direct" },
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
expect(recoveryRuntime.run).toHaveBeenCalledTimes(2);
|
|
515
|
+
expect(store.all()[0].runtimeSessionId).toBe("sid-2");
|
|
516
|
+
expect(channel.sends.map((s) => s.message.text)).toEqual(["ok", "recovered"]);
|
|
517
|
+
});
|
|
518
|
+
|
|
463
519
|
it("treats auth failure text as an error and does not persist the failed session", async () => {
|
|
464
520
|
let callNo = 0;
|
|
465
521
|
const runtimeFactory: RuntimeFactory = () => {
|
|
@@ -986,7 +986,7 @@ function normalizeBlockForHub(
|
|
|
986
986
|
// Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
|
|
987
987
|
// Codex: {type:"item.completed", item:{type:"agent_message", text}}
|
|
988
988
|
// DeepSeek: {event:"message.delta", payload:{content}} or
|
|
989
|
-
// {event:"item.delta", payload:{
|
|
989
|
+
// {event:"item.delta", payload:{kind:"agent_message", delta}}
|
|
990
990
|
let text = "";
|
|
991
991
|
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
992
992
|
for (const c of contents) {
|
|
@@ -999,10 +999,14 @@ function normalizeBlockForHub(
|
|
|
999
999
|
if (
|
|
1000
1000
|
!text &&
|
|
1001
1001
|
raw?.event === "item.delta" &&
|
|
1002
|
-
raw?.payload?.payload?.kind === "agent_message"
|
|
1003
|
-
typeof raw?.payload?.payload?.delta === "string"
|
|
1002
|
+
(raw?.payload?.kind === "agent_message" || raw?.payload?.payload?.kind === "agent_message")
|
|
1004
1003
|
) {
|
|
1005
|
-
text =
|
|
1004
|
+
text =
|
|
1005
|
+
typeof raw?.payload?.delta === "string"
|
|
1006
|
+
? raw.payload.delta
|
|
1007
|
+
: typeof raw?.payload?.payload?.delta === "string"
|
|
1008
|
+
? raw.payload.payload.delta
|
|
1009
|
+
: "";
|
|
1006
1010
|
}
|
|
1007
1011
|
return { kind: "assistant", seq, payload: { text } };
|
|
1008
1012
|
}
|
|
@@ -1200,8 +1204,13 @@ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id
|
|
|
1200
1204
|
};
|
|
1201
1205
|
}
|
|
1202
1206
|
|
|
1203
|
-
if (payload.event === "item.started") {
|
|
1204
|
-
const inner =
|
|
1207
|
+
if (raw?.event === "item.started" || payload.event === "item.started") {
|
|
1208
|
+
const inner =
|
|
1209
|
+
raw?.event === "item.started"
|
|
1210
|
+
? payload
|
|
1211
|
+
: payload.payload && typeof payload.payload === "object"
|
|
1212
|
+
? payload.payload
|
|
1213
|
+
: {};
|
|
1205
1214
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1206
1215
|
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1207
1216
|
return {
|
|
@@ -1240,8 +1249,18 @@ function extractDeepseekToolResult(raw: any): { name?: string; result: string; i
|
|
|
1240
1249
|
};
|
|
1241
1250
|
}
|
|
1242
1251
|
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1252
|
+
if (
|
|
1253
|
+
raw?.event === "item.completed" ||
|
|
1254
|
+
raw?.event === "item.failed" ||
|
|
1255
|
+
payload.event === "item.completed" ||
|
|
1256
|
+
payload.event === "item.failed"
|
|
1257
|
+
) {
|
|
1258
|
+
const inner =
|
|
1259
|
+
raw?.event === "item.completed" || raw?.event === "item.failed"
|
|
1260
|
+
? payload
|
|
1261
|
+
: payload.payload && typeof payload.payload === "object"
|
|
1262
|
+
? payload.payload
|
|
1263
|
+
: {};
|
|
1245
1264
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1246
1265
|
const result =
|
|
1247
1266
|
item?.output ??
|
|
@@ -1273,6 +1292,11 @@ function formatBlockDetails(raw: unknown): string {
|
|
|
1273
1292
|
: typeof r.message === "string" ? r.message
|
|
1274
1293
|
: typeof r.summary === "string" ? r.summary
|
|
1275
1294
|
: typeof r.label === "string" ? r.label
|
|
1295
|
+
: typeof r.payload?.delta === "string" ? r.payload.delta
|
|
1296
|
+
: typeof r.payload?.item?.detail === "string" ? r.payload.item.detail
|
|
1297
|
+
: typeof r.payload?.item?.summary === "string" ? r.payload.item.summary
|
|
1298
|
+
: typeof r.payload?.payload?.item?.detail === "string" ? r.payload.payload.item.detail
|
|
1299
|
+
: typeof r.payload?.payload?.item?.summary === "string" ? r.payload.payload.item.summary
|
|
1276
1300
|
: "";
|
|
1277
1301
|
if (direct) return direct;
|
|
1278
1302
|
|
|
@@ -77,10 +77,28 @@ export class LoginSessionStore {
|
|
|
77
77
|
return session;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Distinguish whether `loginId` is unknown to the store ("missing") vs
|
|
82
|
+
* known-but-past-TTL ("expired"). When the entry is expired this also
|
|
83
|
+
* evicts it from the internal map so callers do not need to follow up
|
|
84
|
+
* with a separate `delete`. Use this when the caller wants to surface
|
|
85
|
+
* a precise error code to the user; prefer `get` when a single nullable
|
|
86
|
+
* result is enough.
|
|
87
|
+
*/
|
|
88
|
+
resolve(loginId: string): { state: "live" | "expired" | "missing"; session?: LoginSession } {
|
|
89
|
+
const s = this.sessions.get(loginId);
|
|
90
|
+
if (!s) return { state: "missing" };
|
|
91
|
+
if (s.expiresAt <= this.now()) {
|
|
92
|
+
this.sessions.delete(loginId);
|
|
93
|
+
return { state: "expired" };
|
|
94
|
+
}
|
|
95
|
+
return { state: "live", session: s };
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
/** Get a non-expired session by id, or `null` when missing/expired. */
|
|
81
99
|
get(loginId: string): LoginSession | null {
|
|
82
|
-
this.
|
|
83
|
-
return
|
|
100
|
+
const { state, session } = this.resolve(loginId);
|
|
101
|
+
return state === "live" && session ? session : null;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/**
|