@botcord/daemon 0.2.78 → 0.2.80
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-singleton.d.ts +13 -0
- package/dist/daemon-singleton.js +68 -0
- package/dist/daemon.js +21 -6
- package/dist/gateway/channels/botcord.d.ts +1 -0
- package/dist/gateway/channels/botcord.js +62 -17
- 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 +56 -13
- package/dist/gateway/types.d.ts +12 -57
- package/dist/gateway-control.js +18 -9
- package/dist/index.js +8 -3
- 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/dist/status-render.d.ts +4 -0
- package/dist/status-render.js +14 -1
- 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-singleton.test.ts +32 -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/__tests__/status-render.test.ts +23 -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-singleton.ts +85 -0
- package/src/daemon.ts +23 -6
- package/src/gateway/__tests__/botcord-channel.test.ts +211 -5
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +263 -0
- package/src/gateway/__tests__/dispatcher.test.ts +56 -0
- package/src/gateway/channels/botcord.ts +69 -17
- 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 +63 -13
- package/src/gateway/types.ts +31 -59
- package/src/gateway-control.ts +21 -9
- package/src/index.ts +9 -2
- package/src/provision.ts +133 -7
- package/src/room-recovery-context.ts +131 -0
- package/src/status-render.ts +14 -1
|
@@ -175,6 +175,232 @@ describe("DeepseekTuiAdapter", () => {
|
|
|
175
175
|
}
|
|
176
176
|
});
|
|
177
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("infers current DeepSeek tool status labels without a payload.tool object", 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.started",
|
|
316
|
+
data: {
|
|
317
|
+
thread_id: "thr_test",
|
|
318
|
+
turn_id: "turn_test",
|
|
319
|
+
event: "item.started",
|
|
320
|
+
payload: {
|
|
321
|
+
item: {
|
|
322
|
+
id: "item_exec",
|
|
323
|
+
kind: "tool_call",
|
|
324
|
+
status: "in_progress",
|
|
325
|
+
summary: "exec_shell started",
|
|
326
|
+
detail: "{\"cmd\":\"botcord-daemon --version\"}",
|
|
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: "done" },
|
|
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, status } = runAdapter(server.baseUrl, server.token);
|
|
348
|
+
await expect(result).resolves.toMatchObject({ text: "done" });
|
|
349
|
+
expect(blocks).toEqual(expect.arrayContaining(["tool_use", "assistant_text"]));
|
|
350
|
+
expect(status).toContainEqual({ phase: "updated", label: "exec_shell" });
|
|
351
|
+
} finally {
|
|
352
|
+
await server.close();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("emits current DeepSeek agent_reasoning completions as thinking blocks", async () => {
|
|
357
|
+
const server = await startMockDeepseekServer({
|
|
358
|
+
events: [
|
|
359
|
+
{
|
|
360
|
+
event: "turn.started",
|
|
361
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.started" },
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
event: "item.completed",
|
|
365
|
+
data: {
|
|
366
|
+
thread_id: "thr_test",
|
|
367
|
+
turn_id: "turn_test",
|
|
368
|
+
event: "item.completed",
|
|
369
|
+
payload: {
|
|
370
|
+
item: {
|
|
371
|
+
id: "item_reasoning",
|
|
372
|
+
kind: "agent_reasoning",
|
|
373
|
+
status: "completed",
|
|
374
|
+
summary: "I should answer briefly.",
|
|
375
|
+
detail: "I should answer briefly.",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
event: "item.delta",
|
|
382
|
+
data: {
|
|
383
|
+
thread_id: "thr_test",
|
|
384
|
+
turn_id: "turn_test",
|
|
385
|
+
event: "item.delta",
|
|
386
|
+
payload: { kind: "agent_message", delta: "hi" },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
event: "turn.completed",
|
|
391
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.completed" },
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
const { result, blocks } = runAdapter(server.baseUrl, server.token);
|
|
397
|
+
await expect(result).resolves.toMatchObject({ text: "hi" });
|
|
398
|
+
expect(blocks).toContain("thinking");
|
|
399
|
+
} finally {
|
|
400
|
+
await server.close();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
178
404
|
it("reuses an existing DeepSeek thread id and patches per-turn system context", async () => {
|
|
179
405
|
const server = await startMockDeepseekServer({ threadId: "thr_existing" });
|
|
180
406
|
try {
|
|
@@ -233,6 +459,43 @@ describe("DeepseekTuiAdapter", () => {
|
|
|
233
459
|
}
|
|
234
460
|
});
|
|
235
461
|
|
|
462
|
+
it("surfaces a diagnostic error when DeepSeek completes a turn with no assistant_message", async () => {
|
|
463
|
+
const server = await startMockDeepseekServer({
|
|
464
|
+
events: [
|
|
465
|
+
{ event: "turn.started", data: { thread_id: "thr_test", turn_id: "turn_test" } },
|
|
466
|
+
{
|
|
467
|
+
event: "item.completed",
|
|
468
|
+
data: {
|
|
469
|
+
thread_id: "thr_test",
|
|
470
|
+
turn_id: "turn_test",
|
|
471
|
+
event: "item.completed",
|
|
472
|
+
payload: {
|
|
473
|
+
item: {
|
|
474
|
+
id: "item_reasoning",
|
|
475
|
+
kind: "agent_reasoning",
|
|
476
|
+
status: "completed",
|
|
477
|
+
summary: "thinking...",
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
event: "turn.completed",
|
|
484
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", payload: { turn: { status: "completed" } } },
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
});
|
|
488
|
+
try {
|
|
489
|
+
const { result } = runAdapter(server.baseUrl, server.token);
|
|
490
|
+
const res = await result;
|
|
491
|
+
expect(res.text).toBe("");
|
|
492
|
+
expect(res.newSessionId).toBe("thr_test");
|
|
493
|
+
expect(res.error).toMatch(/no assistant_message/);
|
|
494
|
+
} finally {
|
|
495
|
+
await server.close();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
236
499
|
it("returns a runtime error when DeepSeek completes the turn as failed", async () => {
|
|
237
500
|
const server = await startMockDeepseekServer({
|
|
238
501
|
events: [
|
|
@@ -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 = () => {
|
|
@@ -977,16 +977,19 @@ export { normalizeBlockForHub as __normalizeBlockForHubForTests };
|
|
|
977
977
|
function normalizeBlockForHub(
|
|
978
978
|
block: { raw?: unknown; kind?: string; seq?: number } | undefined,
|
|
979
979
|
seq: number,
|
|
980
|
-
): { kind: string; seq: number; payload: Record<string, unknown
|
|
980
|
+
): { kind: string; seq: number; payload: Record<string, unknown>; raw?: unknown } {
|
|
981
981
|
const raw = (block?.raw ?? {}) as any;
|
|
982
982
|
const kind = block?.kind ?? "other";
|
|
983
983
|
const payload: Record<string, unknown> = {};
|
|
984
|
+
const withRaw = (out: { kind: string; seq: number; payload: Record<string, unknown> }) => (
|
|
985
|
+
block && "raw" in block ? { ...out, raw: block.raw } : out
|
|
986
|
+
);
|
|
984
987
|
|
|
985
988
|
if (kind === "assistant_text") {
|
|
986
989
|
// Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
|
|
987
990
|
// Codex: {type:"item.completed", item:{type:"agent_message", text}}
|
|
988
991
|
// DeepSeek: {event:"message.delta", payload:{content}} or
|
|
989
|
-
// {event:"item.delta", payload:{
|
|
992
|
+
// {event:"item.delta", payload:{kind:"agent_message", delta}}
|
|
990
993
|
let text = "";
|
|
991
994
|
const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
992
995
|
for (const c of contents) {
|
|
@@ -999,10 +1002,14 @@ function normalizeBlockForHub(
|
|
|
999
1002
|
if (
|
|
1000
1003
|
!text &&
|
|
1001
1004
|
raw?.event === "item.delta" &&
|
|
1002
|
-
raw?.payload?.payload?.kind === "agent_message"
|
|
1003
|
-
typeof raw?.payload?.payload?.delta === "string"
|
|
1005
|
+
(raw?.payload?.kind === "agent_message" || raw?.payload?.payload?.kind === "agent_message")
|
|
1004
1006
|
) {
|
|
1005
|
-
text =
|
|
1007
|
+
text =
|
|
1008
|
+
typeof raw?.payload?.delta === "string"
|
|
1009
|
+
? raw.payload.delta
|
|
1010
|
+
: typeof raw?.payload?.payload?.delta === "string"
|
|
1011
|
+
? raw.payload.payload.delta
|
|
1012
|
+
: "";
|
|
1006
1013
|
}
|
|
1007
1014
|
return { kind: "assistant", seq, payload: { text } };
|
|
1008
1015
|
}
|
|
@@ -1018,7 +1025,7 @@ function normalizeBlockForHub(
|
|
|
1018
1025
|
if (call.id) payload.id = call.id;
|
|
1019
1026
|
if (call.status) payload.status = call.status;
|
|
1020
1027
|
}
|
|
1021
|
-
return { kind: "tool_call", seq, payload };
|
|
1028
|
+
return withRaw({ kind: "tool_call", seq, payload });
|
|
1022
1029
|
}
|
|
1023
1030
|
|
|
1024
1031
|
if (kind === "tool_result") {
|
|
@@ -1028,7 +1035,7 @@ function normalizeBlockForHub(
|
|
|
1028
1035
|
payload.result = result.result;
|
|
1029
1036
|
if (result.id) payload.tool_use_id = result.id;
|
|
1030
1037
|
}
|
|
1031
|
-
return { kind: "tool_result", seq, payload };
|
|
1038
|
+
return withRaw({ kind: "tool_result", seq, payload });
|
|
1032
1039
|
}
|
|
1033
1040
|
|
|
1034
1041
|
if (kind === "system") {
|
|
@@ -1036,7 +1043,7 @@ function normalizeBlockForHub(
|
|
|
1036
1043
|
if (typeof raw?.session_id === "string") payload.session_id = raw.session_id;
|
|
1037
1044
|
if (typeof raw?.model === "string") payload.model = raw.model;
|
|
1038
1045
|
payload.details = formatBlockDetails(raw);
|
|
1039
|
-
return { kind: "system", seq, payload };
|
|
1046
|
+
return withRaw({ kind: "system", seq, payload });
|
|
1040
1047
|
}
|
|
1041
1048
|
|
|
1042
1049
|
if (kind === "thinking") {
|
|
@@ -1048,7 +1055,7 @@ function normalizeBlockForHub(
|
|
|
1048
1055
|
if (typeof raw?.label === "string") payload.label = raw.label;
|
|
1049
1056
|
if (typeof raw?.source === "string") payload.source = raw.source;
|
|
1050
1057
|
payload.details = formatBlockDetails(raw);
|
|
1051
|
-
return { kind: "thinking", seq, payload };
|
|
1058
|
+
return withRaw({ kind: "thinking", seq, payload });
|
|
1052
1059
|
}
|
|
1053
1060
|
|
|
1054
1061
|
// "other" — e.g. Claude Code `type:"result"` end-of-turn summary.
|
|
@@ -1058,14 +1065,14 @@ function normalizeBlockForHub(
|
|
|
1058
1065
|
const event = typeof raw?.event === "string" ? raw.event : undefined;
|
|
1059
1066
|
const embedded = typeof raw?.payload?.event === "string" ? raw.payload.event : undefined;
|
|
1060
1067
|
if (event || embedded) payload.event = event ?? embedded;
|
|
1061
|
-
return { kind: "other", seq, payload };
|
|
1068
|
+
return withRaw({ kind: "other", seq, payload });
|
|
1062
1069
|
}
|
|
1063
1070
|
if (raw?.type === "result") {
|
|
1064
1071
|
if (typeof raw.result === "string") payload.text = raw.result;
|
|
1065
1072
|
if (typeof raw.subtype === "string") payload.subtype = raw.subtype;
|
|
1066
1073
|
if (typeof raw.total_cost_usd === "number") payload.total_cost_usd = raw.total_cost_usd;
|
|
1067
1074
|
}
|
|
1068
|
-
return { kind: "other", seq, payload };
|
|
1075
|
+
return withRaw({ kind: "other", seq, payload });
|
|
1069
1076
|
}
|
|
1070
1077
|
|
|
1071
1078
|
function isTerminalRuntimeBlock(raw: any): boolean {
|
|
@@ -1200,25 +1207,41 @@ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id
|
|
|
1200
1207
|
};
|
|
1201
1208
|
}
|
|
1202
1209
|
|
|
1203
|
-
if (payload.event === "item.started") {
|
|
1204
|
-
const inner =
|
|
1210
|
+
if (raw?.event === "item.started" || payload.event === "item.started") {
|
|
1211
|
+
const inner =
|
|
1212
|
+
raw?.event === "item.started"
|
|
1213
|
+
? payload
|
|
1214
|
+
: payload.payload && typeof payload.payload === "object"
|
|
1215
|
+
? payload.payload
|
|
1216
|
+
: {};
|
|
1205
1217
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1206
1218
|
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1219
|
+
const itemParams = parseMaybeJson(item?.input ?? item?.arguments ?? item?.detail);
|
|
1220
|
+
const detailParams =
|
|
1221
|
+
itemParams !== undefined
|
|
1222
|
+
? itemParams
|
|
1223
|
+
: typeof item?.detail === "string" && item.detail.trim()
|
|
1224
|
+
? item.detail.trim()
|
|
1225
|
+
: undefined;
|
|
1207
1226
|
return {
|
|
1208
1227
|
name:
|
|
1209
1228
|
stringField(tool, "name") ??
|
|
1210
1229
|
stringField(inner, "name") ??
|
|
1211
1230
|
stringField(item, "name") ??
|
|
1231
|
+
inferDeepseekToolName(item) ??
|
|
1212
1232
|
stringField(item, "type") ??
|
|
1213
1233
|
"tool",
|
|
1214
1234
|
params: parseMaybeJson(
|
|
1215
1235
|
tool?.input ??
|
|
1216
1236
|
tool?.rawInput ??
|
|
1217
1237
|
tool?.arguments ??
|
|
1238
|
+
tool?.params ??
|
|
1218
1239
|
inner.input ??
|
|
1240
|
+
inner.arguments ??
|
|
1241
|
+
inner.params ??
|
|
1219
1242
|
item?.input ??
|
|
1220
1243
|
item?.arguments,
|
|
1221
|
-
) ?? tool ?? item,
|
|
1244
|
+
) ?? detailParams ?? tool ?? item,
|
|
1222
1245
|
id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
|
|
1223
1246
|
status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
|
|
1224
1247
|
};
|
|
@@ -1240,8 +1263,18 @@ function extractDeepseekToolResult(raw: any): { name?: string; result: string; i
|
|
|
1240
1263
|
};
|
|
1241
1264
|
}
|
|
1242
1265
|
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1266
|
+
if (
|
|
1267
|
+
raw?.event === "item.completed" ||
|
|
1268
|
+
raw?.event === "item.failed" ||
|
|
1269
|
+
payload.event === "item.completed" ||
|
|
1270
|
+
payload.event === "item.failed"
|
|
1271
|
+
) {
|
|
1272
|
+
const inner =
|
|
1273
|
+
raw?.event === "item.completed" || raw?.event === "item.failed"
|
|
1274
|
+
? payload
|
|
1275
|
+
: payload.payload && typeof payload.payload === "object"
|
|
1276
|
+
? payload.payload
|
|
1277
|
+
: {};
|
|
1245
1278
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1246
1279
|
const result =
|
|
1247
1280
|
item?.output ??
|
|
@@ -1256,7 +1289,11 @@ function extractDeepseekToolResult(raw: any): { name?: string; result: string; i
|
|
|
1256
1289
|
item ??
|
|
1257
1290
|
inner;
|
|
1258
1291
|
return {
|
|
1259
|
-
name:
|
|
1292
|
+
name:
|
|
1293
|
+
stringField(item, "name") ??
|
|
1294
|
+
inferDeepseekToolName(item) ??
|
|
1295
|
+
stringField(inner, "name") ??
|
|
1296
|
+
stringField(item, "type"),
|
|
1260
1297
|
result: stringifyToolResult(result),
|
|
1261
1298
|
id: stringField(item, "id") ?? stringField(inner, "id"),
|
|
1262
1299
|
};
|
|
@@ -1273,6 +1310,11 @@ function formatBlockDetails(raw: unknown): string {
|
|
|
1273
1310
|
: typeof r.message === "string" ? r.message
|
|
1274
1311
|
: typeof r.summary === "string" ? r.summary
|
|
1275
1312
|
: typeof r.label === "string" ? r.label
|
|
1313
|
+
: typeof r.payload?.delta === "string" ? r.payload.delta
|
|
1314
|
+
: typeof r.payload?.item?.detail === "string" ? r.payload.item.detail
|
|
1315
|
+
: typeof r.payload?.item?.summary === "string" ? r.payload.item.summary
|
|
1316
|
+
: typeof r.payload?.payload?.item?.detail === "string" ? r.payload.payload.item.detail
|
|
1317
|
+
: typeof r.payload?.payload?.item?.summary === "string" ? r.payload.payload.item.summary
|
|
1276
1318
|
: "";
|
|
1277
1319
|
if (direct) return direct;
|
|
1278
1320
|
|
|
@@ -1368,6 +1410,16 @@ function parseMaybeJson(value: unknown): unknown {
|
|
|
1368
1410
|
}
|
|
1369
1411
|
}
|
|
1370
1412
|
|
|
1413
|
+
function inferDeepseekToolName(item: any): string | undefined {
|
|
1414
|
+
const candidates = [stringField(item, "summary"), stringField(item, "detail")];
|
|
1415
|
+
for (const candidate of candidates) {
|
|
1416
|
+
if (!candidate) continue;
|
|
1417
|
+
const match = candidate.match(/^([A-Za-z0-9_.:-]+)\s*(?:started|completed|failed|returned|:)/);
|
|
1418
|
+
if (match?.[1] && match[1] !== "tool_call") return match[1];
|
|
1419
|
+
}
|
|
1420
|
+
return undefined;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1371
1423
|
function isEmptyRecord(value: unknown): boolean {
|
|
1372
1424
|
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
1373
1425
|
}
|
|
@@ -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
|
/**
|
|
@@ -1,68 +1,10 @@
|
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/<\/?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,
|
|
19
|
-
"[⚠ stripped: agent-message tag]",
|
|
20
|
-
);
|
|
21
|
-
s = s.replace(
|
|
22
|
-
/<\/?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,
|
|
23
|
-
"[⚠ stripped: human-message tag]",
|
|
24
|
-
);
|
|
25
|
-
s = s.replace(
|
|
26
|
-
/<\/?r[\s]*o[\s]*o[\s]*m[\s]*-[\s]*r[\s]*u[\s]*l[\s]*e[\s\S]*?>/gi,
|
|
27
|
-
"[⚠ stripped: room-rule tag]",
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
return s
|
|
31
|
-
.split(/\r?\n/)
|
|
32
|
-
.map((line) => {
|
|
33
|
-
let l = line;
|
|
34
|
-
l = l.replace(/^\[(BotCord (?:Message|Notification))\]/i, "[⚠ fake: $1]");
|
|
35
|
-
l = l.replace(/^\[Room Rule\]/i, "[⚠ fake: Room Rule]");
|
|
36
|
-
l = l.replace(/^\[房间规则\]/i, "[⚠ fake: 房间规则]");
|
|
37
|
-
l = l.replace(/^\[系统提示\]/i, "[⚠ fake: 系统提示]");
|
|
38
|
-
l = l.replace(/^\[BotCord\s+([^\]\r\n]+)\]/i, (_m, label) => {
|
|
39
|
-
const head = String(label).split(":")[0].trim() || String(label).trim();
|
|
40
|
-
return `[⚠ fake: BotCord ${head}]`;
|
|
41
|
-
});
|
|
42
|
-
l = l.replace(/^\[(System|SYSTEM|Assistant|ASSISTANT|User|USER)\]/, "[⚠ fake: $1]");
|
|
43
|
-
l = l.replace(/<\/?\s*system(?:-reminder)?\s*>/gi, "[⚠ stripped: system tag]");
|
|
44
|
-
l = l.replace(/<\|im_start\|>/gi, "[⚠ stripped: im_start]");
|
|
45
|
-
l = l.replace(/<\|im_end\|>/gi, "[⚠ stripped: im_end]");
|
|
46
|
-
l = l.replace(/\[\/?INST\]/gi, "[⚠ stripped: INST]");
|
|
47
|
-
l = l.replace(/<<\/?SYS>>/gi, "[⚠ stripped: SYS]");
|
|
48
|
-
l = l.replace(/<\s*\/?\|(?:system|user|assistant)\|?\s*>/gi, "[⚠ stripped: role tag]");
|
|
49
|
-
return l;
|
|
50
|
-
})
|
|
51
|
-
.join("\n");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Sanitize a sender label so it's safe to embed inside
|
|
56
|
-
* `<agent-message sender="...">`. Must not contain newlines, structural
|
|
57
|
-
* markers, or characters that could break the XML attribute boundary.
|
|
58
|
-
*/
|
|
59
|
-
export function sanitizeSenderName(name: string): string {
|
|
60
|
-
return name
|
|
61
|
-
.replace(/[\n\r]/g, " ")
|
|
62
|
-
.replace(/\[/g, "⟦")
|
|
63
|
-
.replace(/\]/g, "⟧")
|
|
64
|
-
.replace(/"/g, "'")
|
|
65
|
-
.replace(/</g, "<")
|
|
66
|
-
.replace(/>/g, ">")
|
|
67
|
-
.slice(0, 100);
|
|
68
|
-
}
|
|
7
|
+
export {
|
|
8
|
+
sanitizeUntrustedContent,
|
|
9
|
+
sanitizeSenderName,
|
|
10
|
+
} from "@botcord/protocol-core";
|