@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.
Files changed (61) hide show
  1. package/dist/attention-policy-fetcher.d.ts +14 -0
  2. package/dist/attention-policy-fetcher.js +59 -0
  3. package/dist/cloud-daemon.js +8 -0
  4. package/dist/cloud-gateway-runtime.d.ts +29 -0
  5. package/dist/cloud-gateway-runtime.js +122 -0
  6. package/dist/daemon-singleton.d.ts +13 -0
  7. package/dist/daemon-singleton.js +68 -0
  8. package/dist/daemon.js +21 -6
  9. package/dist/gateway/channels/botcord.d.ts +1 -0
  10. package/dist/gateway/channels/botcord.js +62 -17
  11. package/dist/gateway/channels/login-session.d.ts +12 -0
  12. package/dist/gateway/channels/login-session.js +20 -2
  13. package/dist/gateway/channels/sanitize.d.ts +5 -18
  14. package/dist/gateway/channels/sanitize.js +5 -54
  15. package/dist/gateway/channels/text-split.d.ts +5 -11
  16. package/dist/gateway/channels/text-split.js +5 -31
  17. package/dist/gateway/dispatcher.d.ts +7 -1
  18. package/dist/gateway/dispatcher.js +88 -8
  19. package/dist/gateway/gateway.d.ts +16 -1
  20. package/dist/gateway/gateway.js +21 -0
  21. package/dist/gateway/policy-resolver.js +17 -9
  22. package/dist/gateway/runtimes/deepseek-tui.js +56 -13
  23. package/dist/gateway/types.d.ts +12 -57
  24. package/dist/gateway-control.js +18 -9
  25. package/dist/index.js +8 -3
  26. package/dist/provision.d.ts +7 -3
  27. package/dist/provision.js +115 -8
  28. package/dist/room-recovery-context.d.ts +11 -0
  29. package/dist/room-recovery-context.js +97 -0
  30. package/dist/status-render.d.ts +4 -0
  31. package/dist/status-render.js +14 -1
  32. package/package.json +2 -2
  33. package/src/__tests__/attention-policy-fetcher.test.ts +67 -0
  34. package/src/__tests__/cloud-gateway-runtime.test.ts +127 -0
  35. package/src/__tests__/daemon-singleton.test.ts +32 -0
  36. package/src/__tests__/gateway-control.test.ts +136 -0
  37. package/src/__tests__/policy-resolver.test.ts +20 -0
  38. package/src/__tests__/provision.test.ts +65 -0
  39. package/src/__tests__/status-render.test.ts +23 -0
  40. package/src/attention-policy-fetcher.ts +87 -0
  41. package/src/cloud-daemon.ts +8 -0
  42. package/src/cloud-gateway-runtime.ts +171 -0
  43. package/src/daemon-singleton.ts +85 -0
  44. package/src/daemon.ts +23 -6
  45. package/src/gateway/__tests__/botcord-channel.test.ts +211 -5
  46. package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +263 -0
  47. package/src/gateway/__tests__/dispatcher.test.ts +56 -0
  48. package/src/gateway/channels/botcord.ts +69 -17
  49. package/src/gateway/channels/login-session.ts +20 -2
  50. package/src/gateway/channels/sanitize.ts +8 -66
  51. package/src/gateway/channels/text-split.ts +5 -27
  52. package/src/gateway/dispatcher.ts +123 -27
  53. package/src/gateway/gateway.ts +29 -0
  54. package/src/gateway/policy-resolver.ts +20 -9
  55. package/src/gateway/runtimes/deepseek-tui.ts +63 -13
  56. package/src/gateway/types.ts +31 -59
  57. package/src/gateway-control.ts +21 -9
  58. package/src/index.ts +9 -2
  59. package/src/provision.ts +133 -7
  60. package/src/room-recovery-context.ts +131 -0
  61. package/src/status-render.ts +14 -1
@@ -0,0 +1,14 @@
1
+ import type { DaemonAttentionPolicy } from "./gateway/policy-resolver.js";
2
+ export interface AttentionPolicyFetcherOptions {
3
+ credentialPathByAgentId: Map<string, string>;
4
+ defaultCredentialsPath?: string;
5
+ hubBaseUrl?: string;
6
+ log?: {
7
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
8
+ };
9
+ }
10
+ export type AttentionPolicyFetcher = (args: {
11
+ agentId: string;
12
+ roomId?: string | null;
13
+ }) => Promise<DaemonAttentionPolicy | undefined>;
14
+ export declare function createAttentionPolicyFetcher(opts: AttentionPolicyFetcherOptions): AttentionPolicyFetcher;
@@ -0,0 +1,59 @@
1
+ import { BotCordClient, defaultCredentialsFile, loadStoredCredentials, updateCredentialsToken, } from "@botcord/protocol-core";
2
+ export function createAttentionPolicyFetcher(opts) {
3
+ const clients = new Map();
4
+ function getClient(agentId) {
5
+ const existing = clients.get(agentId);
6
+ if (existing)
7
+ return existing.client;
8
+ const credentialsPath = opts.credentialPathByAgentId.get(agentId) ??
9
+ opts.defaultCredentialsPath ??
10
+ defaultCredentialsFile(agentId);
11
+ try {
12
+ const creds = loadStoredCredentials(credentialsPath);
13
+ const client = new BotCordClient({
14
+ hubUrl: opts.hubBaseUrl ?? creds.hubUrl,
15
+ agentId: creds.agentId,
16
+ keyId: creds.keyId,
17
+ privateKey: creds.privateKey,
18
+ ...(creds.token ? { token: creds.token } : {}),
19
+ ...(creds.tokenExpiresAt !== undefined
20
+ ? { tokenExpiresAt: creds.tokenExpiresAt }
21
+ : {}),
22
+ });
23
+ client.onTokenRefresh = (token, expiresAt) => {
24
+ try {
25
+ updateCredentialsToken(credentialsPath, token, expiresAt);
26
+ }
27
+ catch {
28
+ // Persistence failures are non-fatal; the next refresh retries.
29
+ }
30
+ };
31
+ clients.set(agentId, { client, credentialsPath });
32
+ return client;
33
+ }
34
+ catch (err) {
35
+ opts.log?.warn("daemon.attention-policy.client-init-failed", {
36
+ agentId,
37
+ credentialsPath,
38
+ error: err instanceof Error ? err.message : String(err),
39
+ });
40
+ return null;
41
+ }
42
+ }
43
+ return async ({ agentId, roomId }) => {
44
+ const client = getClient(agentId);
45
+ if (!client)
46
+ return undefined;
47
+ try {
48
+ return await client.getAttentionPolicy({ roomId });
49
+ }
50
+ catch (err) {
51
+ opts.log?.warn("daemon.attention-policy.fetch-failed", {
52
+ agentId,
53
+ roomId: roomId ?? null,
54
+ error: err instanceof Error ? err.message : String(err),
55
+ });
56
+ return undefined;
57
+ }
58
+ };
59
+ }
@@ -23,6 +23,7 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
23
23
  import { readWorkingMemorySnapshot } from "./working-memory.js";
24
24
  import { createRoomStaticContextBuilder } from "./room-context.js";
25
25
  import { createRoomContextFetcher } from "./room-context-fetcher.js";
26
+ import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
26
27
  import { composeBotCordUserTurn } from "./turn-text.js";
27
28
  import { PolicyResolver } from "./gateway/policy-resolver.js";
28
29
  import { scanMention } from "./mention-scan.js";
@@ -92,6 +93,12 @@ export async function startCloudDaemon(opts) {
92
93
  fetchRoomInfo: roomContextFetcher,
93
94
  log: logger,
94
95
  });
96
+ const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
97
+ credentialPathByAgentId,
98
+ hubBaseUrl: cloudCfg.hubUrl,
99
+ limit: 20,
100
+ log: logger,
101
+ });
95
102
  const scBuilders = new Map();
96
103
  const buildSystemContext = (message) => {
97
104
  const b = scBuilders.get(message.accountId);
@@ -180,6 +187,7 @@ export async function startCloudDaemon(opts) {
180
187
  turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
181
188
  buildSystemContext,
182
189
  buildMemoryContext,
190
+ buildRuntimeRecoveryContext,
183
191
  onInbound,
184
192
  onTurnComplete,
185
193
  composeUserTurn: composeBotCordUserTurn,
@@ -0,0 +1,29 @@
1
+ import { type GatewayInboundFrame } from "@botcord/protocol-core";
2
+ import type { Gateway, GatewayLogger } from "./gateway/index.js";
3
+ export interface CloudGatewayRuntimeResult {
4
+ accepted: boolean;
5
+ eventId: string;
6
+ gatewayId: string;
7
+ agentId: string;
8
+ conversationId: string;
9
+ turnId: string;
10
+ outbound?: {
11
+ finalText: string;
12
+ providerMessageId?: string | null;
13
+ };
14
+ error?: {
15
+ code: string;
16
+ message: string;
17
+ };
18
+ }
19
+ /**
20
+ * Execute one ingress-originated runtime frame through the normal Gateway
21
+ * dispatcher while keeping provider I/O outside the cloud sandbox.
22
+ *
23
+ * The temporary channel is scoped to this call and implements only the
24
+ * dispatcher-facing send/status surface. Its send() method captures the
25
+ * final runtime reply; the Hub relay converts that into
26
+ * gateway_outbound_complete for gateway-ingress, which then calls the real
27
+ * provider API.
28
+ */
29
+ export declare function handleCloudGatewayRuntimeInbound(gateway: Gateway, frame: GatewayInboundFrame, log?: GatewayLogger): Promise<CloudGatewayRuntimeResult>;
@@ -0,0 +1,122 @@
1
+ import { RUNTIME_FRAME_TYPES, } from "@botcord/protocol-core";
2
+ /**
3
+ * Execute one ingress-originated runtime frame through the normal Gateway
4
+ * dispatcher while keeping provider I/O outside the cloud sandbox.
5
+ *
6
+ * The temporary channel is scoped to this call and implements only the
7
+ * dispatcher-facing send/status surface. Its send() method captures the
8
+ * final runtime reply; the Hub relay converts that into
9
+ * gateway_outbound_complete for gateway-ingress, which then calls the real
10
+ * provider API.
11
+ */
12
+ export async function handleCloudGatewayRuntimeInbound(gateway, frame, log) {
13
+ if (frame.type !== RUNTIME_FRAME_TYPES.GATEWAY_INBOUND) {
14
+ return rejected(frame, "bad_frame_type", `unsupported frame type "${frame.type}"`);
15
+ }
16
+ if (!frame.gateway_id || !frame.agent_id || !frame.event_id) {
17
+ return rejected(frame, "bad_frame", "gateway_id, agent_id and event_id are required");
18
+ }
19
+ if (frame.message.accountId !== frame.agent_id) {
20
+ return rejected(frame, "account_mismatch", "message.accountId does not match frame.agent_id");
21
+ }
22
+ if (frame.message.channel !== frame.gateway_id) {
23
+ return rejected(frame, "channel_mismatch", "message.channel does not match frame.gateway_id");
24
+ }
25
+ let accepted = false;
26
+ let outboundText = null;
27
+ let providerMessageId;
28
+ const channel = createRuntimeRelayChannel({
29
+ id: frame.gateway_id,
30
+ provider: frame.provider,
31
+ accountId: frame.agent_id,
32
+ onSend: async (ctx) => {
33
+ outboundText = ctx.message.text ?? "";
34
+ providerMessageId = ctx.message.traceId ?? null;
35
+ return { providerMessageId };
36
+ },
37
+ });
38
+ const message = {
39
+ ...frame.message,
40
+ raw: {
41
+ source_type: "cloud_gateway_ingress",
42
+ provider: frame.provider,
43
+ event_id: frame.event_id,
44
+ gateway_id: frame.gateway_id,
45
+ },
46
+ };
47
+ try {
48
+ await gateway.injectInboundThrough(message, channel, {
49
+ accept: async () => {
50
+ accepted = true;
51
+ },
52
+ });
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ log?.warn("cloud gateway runtime dispatch failed", {
57
+ eventId: frame.event_id,
58
+ gatewayId: frame.gateway_id,
59
+ agentId: frame.agent_id,
60
+ error: message,
61
+ });
62
+ return rejected(frame, "dispatch_failed", message);
63
+ }
64
+ return {
65
+ accepted,
66
+ eventId: frame.event_id,
67
+ gatewayId: frame.gateway_id,
68
+ agentId: frame.agent_id,
69
+ conversationId: frame.message.conversation.id,
70
+ turnId: `turn_${frame.event_id}`,
71
+ ...(outboundText !== null
72
+ ? {
73
+ outbound: {
74
+ finalText: outboundText,
75
+ providerMessageId: providerMessageId ?? null,
76
+ },
77
+ }
78
+ : {}),
79
+ ...(!accepted
80
+ ? { error: { code: "not_accepted", message: "dispatcher did not accept inbound" } }
81
+ : {}),
82
+ };
83
+ }
84
+ function createRuntimeRelayChannel(opts) {
85
+ let lastSendAt;
86
+ return {
87
+ id: opts.id,
88
+ type: opts.provider,
89
+ async start() {
90
+ return undefined;
91
+ },
92
+ async stop() {
93
+ return undefined;
94
+ },
95
+ async send(ctx) {
96
+ lastSendAt = Date.now();
97
+ return opts.onSend(ctx);
98
+ },
99
+ status() {
100
+ return {
101
+ channel: opts.id,
102
+ accountId: opts.accountId,
103
+ running: true,
104
+ connected: true,
105
+ authorized: true,
106
+ provider: opts.provider,
107
+ ...(lastSendAt ? { lastSendAt } : {}),
108
+ };
109
+ },
110
+ };
111
+ }
112
+ function rejected(frame, code, message) {
113
+ return {
114
+ accepted: false,
115
+ eventId: frame.event_id ?? "",
116
+ gatewayId: frame.gateway_id ?? "",
117
+ agentId: frame.agent_id ?? "",
118
+ conversationId: frame.message?.conversation.id ?? "",
119
+ turnId: frame.event_id ? `turn_${frame.event_id}` : "turn_unknown",
120
+ error: { code, message },
121
+ };
122
+ }
@@ -4,6 +4,14 @@ export interface SingletonLogger {
4
4
  }
5
5
  export declare function readPid(pidPath?: string): number | null;
6
6
  export declare function pidAlive(pid: number): boolean;
7
+ export interface DaemonProcessInfo {
8
+ pid: number;
9
+ command: string;
10
+ }
11
+ export declare function parseDaemonProcesses(psOutput: string, currentPid?: number): DaemonProcessInfo[];
12
+ export declare function findOtherDaemonProcesses(opts?: {
13
+ currentPid?: number;
14
+ }): DaemonProcessInfo[];
7
15
  export declare function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean>;
8
16
  export declare function stopExistingDaemonForRestart(pid: number, opts?: {
9
17
  pidPath?: string;
@@ -15,6 +23,11 @@ export declare function stopDaemonFromPidFileForRestart(opts?: {
15
23
  currentPid?: number;
16
24
  logger?: SingletonLogger;
17
25
  }): Promise<void>;
26
+ export declare function stopOtherDaemonProcessesForRestart(opts?: {
27
+ currentPid?: number;
28
+ logger?: SingletonLogger;
29
+ processes?: DaemonProcessInfo[];
30
+ }): Promise<DaemonProcessInfo[]>;
18
31
  export declare function ensureNoOtherDaemonFromPidFile(opts?: {
19
32
  pidPath?: string;
20
33
  currentPid?: number;
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
3
  import { PID_PATH } from "./config.js";
3
4
  const noopLogger = {
@@ -24,6 +25,36 @@ export function pidAlive(pid) {
24
25
  return false;
25
26
  }
26
27
  }
28
+ export function parseDaemonProcesses(psOutput, currentPid = process.pid) {
29
+ const out = [];
30
+ for (const line of psOutput.split(/\r?\n/)) {
31
+ const trimmed = line.trim();
32
+ const match = /^(\d+)\s+(.+)$/.exec(trimmed);
33
+ if (!match)
34
+ continue;
35
+ const pid = Number(match[1]);
36
+ if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid)
37
+ continue;
38
+ const command = match[2] ?? "";
39
+ if (!isBotCordDaemonStartCommand(command))
40
+ continue;
41
+ out.push({ pid, command });
42
+ }
43
+ return out;
44
+ }
45
+ export function findOtherDaemonProcesses(opts = {}) {
46
+ const currentPid = opts.currentPid ?? process.pid;
47
+ try {
48
+ const output = execFileSync("ps", ["-axo", "pid=,command="], {
49
+ encoding: "utf8",
50
+ stdio: ["ignore", "pipe", "ignore"],
51
+ });
52
+ return parseDaemonProcesses(output, currentPid).filter((p) => pidAlive(p.pid));
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
27
58
  export async function waitForPidExit(pid, timeoutMs) {
28
59
  const deadline = Date.now() + timeoutMs;
29
60
  while (Date.now() < deadline) {
@@ -66,6 +97,36 @@ export async function stopDaemonFromPidFileForRestart(opts = {}) {
66
97
  await stopExistingDaemonForRestart(existing, opts);
67
98
  }
68
99
  }
100
+ export async function stopOtherDaemonProcessesForRestart(opts = {}) {
101
+ const currentPid = opts.currentPid ?? process.pid;
102
+ const logger = opts.logger ?? noopLogger;
103
+ const processes = opts.processes ?? findOtherDaemonProcesses({ currentPid });
104
+ for (const proc of processes) {
105
+ logger.info("additional daemon process found; restarting", {
106
+ pid: proc.pid,
107
+ command: proc.command,
108
+ });
109
+ try {
110
+ process.kill(proc.pid, "SIGTERM");
111
+ }
112
+ catch {
113
+ continue;
114
+ }
115
+ if (!(await waitForPidExit(proc.pid, 5_000))) {
116
+ logger.warn("additional daemon did not stop after SIGTERM; sending SIGKILL", {
117
+ pid: proc.pid,
118
+ });
119
+ try {
120
+ process.kill(proc.pid, "SIGKILL");
121
+ }
122
+ catch {
123
+ // ignore
124
+ }
125
+ await waitForPidExit(proc.pid, 2_000);
126
+ }
127
+ }
128
+ return processes;
129
+ }
69
130
  export function ensureNoOtherDaemonFromPidFile(opts = {}) {
70
131
  const pidPath = opts.pidPath ?? PID_PATH;
71
132
  const currentPid = opts.currentPid ?? process.pid;
@@ -86,6 +147,13 @@ export function removePidFile(pidPath = PID_PATH) {
86
147
  // ignore
87
148
  }
88
149
  }
150
+ function isBotCordDaemonStartCommand(command) {
151
+ if (!/\bstart\b/.test(command))
152
+ return false;
153
+ return (command.includes("botcord-daemon") ||
154
+ /(?:^|\s)\S*botcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
155
+ /(?:^|\s)\S*packages\/daemon\/dist\/index\.js(?:\s|$)/.test(command));
156
+ }
89
157
  function delay(ms) {
90
158
  return new Promise((resolve) => setTimeout(resolve, ms));
91
159
  }
package/dist/daemon.js CHANGED
@@ -14,12 +14,14 @@ import { createDaemonSystemContextBuilder } from "./system-context.js";
14
14
  import { readWorkingMemorySnapshot } from "./working-memory.js";
15
15
  import { createRoomStaticContextBuilder } from "./room-context.js";
16
16
  import { createRoomContextFetcher } from "./room-context-fetcher.js";
17
+ import { createRecentRoomMessagesRecoveryBuilder } from "./room-recovery-context.js";
17
18
  import { buildLoopRiskPrompt, loopRiskSessionKey, recordInboundText as recordLoopRiskInbound, recordOutboundText as recordLoopRiskOutbound, } from "./loop-risk.js";
18
19
  import { composeBotCordUserTurn } from "./turn-text.js";
19
20
  import { UserAuthManager } from "./user-auth.js";
20
21
  import { PolicyResolver } from "./gateway/policy-resolver.js";
21
22
  import { scanMention } from "./mention-scan.js";
22
23
  import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
24
+ import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
23
25
  /**
24
26
  * Default hard cap for a single runtime turn. Long-running coding/research
25
27
  * tasks routinely exceed 10 minutes, so daemon-hosted agents get a larger
@@ -233,6 +235,13 @@ export async function startDaemon(opts) {
233
235
  fetchRoomInfo: roomContextFetcher,
234
236
  log: logger,
235
237
  });
238
+ const buildRuntimeRecoveryContext = createRecentRoomMessagesRecoveryBuilder({
239
+ credentialPathByAgentId,
240
+ ...(opts.credentialsPath ? { defaultCredentialsPath: opts.credentialsPath } : {}),
241
+ ...(opts.hubBaseUrl ? { hubBaseUrl: opts.hubBaseUrl } : {}),
242
+ limit: 20,
243
+ log: logger,
244
+ });
236
245
  const scBuilders = new Map();
237
246
  const loopRiskBuilder = (msg) => buildLoopRiskPrompt({
238
247
  sessionKey: loopRiskSessionKey({
@@ -296,13 +305,18 @@ export async function startDaemon(opts) {
296
305
  text: out.text,
297
306
  });
298
307
  };
299
- // Per-agent attention policy cache (PR3, design §4.2 / §5). Seeded from
300
- // the optional `defaultAttention` / `attentionKeywords` carried by
301
- // `provision_agent`, refreshed in-place by the `policy_updated` control
302
- // frame. PR2 will plug per-room overrides into `fetchEffective`; PR3
303
- // leaves it absent so the resolver collapses to per-agent state.
308
+ // Per-agent attention policy cache (design §4.2 / §5). It is seeded from
309
+ // `provision_agent` / `policy_updated` frames when available and falls back
310
+ // to Hub on cold misses, so daemon restarts preserve dashboard policy.
311
+ const fetchAttentionPolicy = createAttentionPolicyFetcher({
312
+ credentialPathByAgentId,
313
+ defaultCredentialsPath: opts.credentialsPath,
314
+ hubBaseUrl: opts.hubBaseUrl,
315
+ log: logger,
316
+ });
304
317
  const policyResolver = new PolicyResolver({
305
- fetchGlobal: async (_agentId) => undefined,
318
+ fetchGlobal: async (agentId) => fetchAttentionPolicy({ agentId, roomId: null }),
319
+ fetchEffective: async (agentId, roomId) => fetchAttentionPolicy({ agentId, roomId }),
306
320
  });
307
321
  // Display-name lookup for the mention text-fallback. Populated from boot
308
322
  // credentials; multi-agent daemons can reuse the same map via accountId.
@@ -374,6 +388,7 @@ export async function startDaemon(opts) {
374
388
  turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
375
389
  buildSystemContext,
376
390
  buildMemoryContext,
391
+ buildRuntimeRecoveryContext,
377
392
  onInbound,
378
393
  onOutbound,
379
394
  onRuntimeCircuitBreakerChange: pushLiveRuntimeSnapshot,
@@ -133,4 +133,5 @@ declare function normalizeBlockForHub(block: {
133
133
  kind: string;
134
134
  seq: number;
135
135
  payload: Record<string, unknown>;
136
+ raw?: unknown;
136
137
  };
@@ -838,11 +838,12 @@ function normalizeBlockForHub(block, seq) {
838
838
  const raw = (block?.raw ?? {});
839
839
  const kind = block?.kind ?? "other";
840
840
  const payload = {};
841
+ const withRaw = (out) => (block && "raw" in block ? { ...out, raw: block.raw } : out);
841
842
  if (kind === "assistant_text") {
842
843
  // Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
843
844
  // Codex: {type:"item.completed", item:{type:"agent_message", text}}
844
845
  // DeepSeek: {event:"message.delta", payload:{content}} or
845
- // {event:"item.delta", payload:{payload:{kind:"agent_message", delta}}}
846
+ // {event:"item.delta", payload:{kind:"agent_message", delta}}
846
847
  let text = "";
847
848
  const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
848
849
  for (const c of contents) {
@@ -856,9 +857,13 @@ function normalizeBlockForHub(block, seq) {
856
857
  }
857
858
  if (!text &&
858
859
  raw?.event === "item.delta" &&
859
- raw?.payload?.payload?.kind === "agent_message" &&
860
- typeof raw?.payload?.payload?.delta === "string") {
861
- text = raw.payload.payload.delta;
860
+ (raw?.payload?.kind === "agent_message" || raw?.payload?.payload?.kind === "agent_message")) {
861
+ text =
862
+ typeof raw?.payload?.delta === "string"
863
+ ? raw.payload.delta
864
+ : typeof raw?.payload?.payload?.delta === "string"
865
+ ? raw.payload.payload.delta
866
+ : "";
862
867
  }
863
868
  return { kind: "assistant", seq, payload: { text } };
864
869
  }
@@ -876,7 +881,7 @@ function normalizeBlockForHub(block, seq) {
876
881
  if (call.status)
877
882
  payload.status = call.status;
878
883
  }
879
- return { kind: "tool_call", seq, payload };
884
+ return withRaw({ kind: "tool_call", seq, payload });
880
885
  }
881
886
  if (kind === "tool_result") {
882
887
  const result = extractToolResult(raw);
@@ -887,7 +892,7 @@ function normalizeBlockForHub(block, seq) {
887
892
  if (result.id)
888
893
  payload.tool_use_id = result.id;
889
894
  }
890
- return { kind: "tool_result", seq, payload };
895
+ return withRaw({ kind: "tool_result", seq, payload });
891
896
  }
892
897
  if (kind === "system") {
893
898
  if (typeof raw?.subtype === "string")
@@ -897,7 +902,7 @@ function normalizeBlockForHub(block, seq) {
897
902
  if (typeof raw?.model === "string")
898
903
  payload.model = raw.model;
899
904
  payload.details = formatBlockDetails(raw);
900
- return { kind: "system", seq, payload };
905
+ return withRaw({ kind: "system", seq, payload });
901
906
  }
902
907
  if (kind === "thinking") {
903
908
  // Daemon-synthesized lifecycle marker. `raw` carries `{ phase, label?, source? }`
@@ -911,7 +916,7 @@ function normalizeBlockForHub(block, seq) {
911
916
  if (typeof raw?.source === "string")
912
917
  payload.source = raw.source;
913
918
  payload.details = formatBlockDetails(raw);
914
- return { kind: "thinking", seq, payload };
919
+ return withRaw({ kind: "thinking", seq, payload });
915
920
  }
916
921
  // "other" — e.g. Claude Code `type:"result"` end-of-turn summary.
917
922
  if (isTerminalRuntimeBlock(raw)) {
@@ -921,7 +926,7 @@ function normalizeBlockForHub(block, seq) {
921
926
  const embedded = typeof raw?.payload?.event === "string" ? raw.payload.event : undefined;
922
927
  if (event || embedded)
923
928
  payload.event = event ?? embedded;
924
- return { kind: "other", seq, payload };
929
+ return withRaw({ kind: "other", seq, payload });
925
930
  }
926
931
  if (raw?.type === "result") {
927
932
  if (typeof raw.result === "string")
@@ -931,7 +936,7 @@ function normalizeBlockForHub(block, seq) {
931
936
  if (typeof raw.total_cost_usd === "number")
932
937
  payload.total_cost_usd = raw.total_cost_usd;
933
938
  }
934
- return { kind: "other", seq, payload };
939
+ return withRaw({ kind: "other", seq, payload });
935
940
  }
936
941
  function isTerminalRuntimeBlock(raw) {
937
942
  const event = typeof raw?.event === "string" ? raw.event : undefined;
@@ -1048,22 +1053,36 @@ function extractDeepseekToolCall(raw) {
1048
1053
  status: stringField(payload, "status") ?? stringField(tool, "status"),
1049
1054
  };
1050
1055
  }
1051
- if (payload.event === "item.started") {
1052
- const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1056
+ if (raw?.event === "item.started" || payload.event === "item.started") {
1057
+ const inner = raw?.event === "item.started"
1058
+ ? payload
1059
+ : payload.payload && typeof payload.payload === "object"
1060
+ ? payload.payload
1061
+ : {};
1053
1062
  const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1054
1063
  const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
1064
+ const itemParams = parseMaybeJson(item?.input ?? item?.arguments ?? item?.detail);
1065
+ const detailParams = itemParams !== undefined
1066
+ ? itemParams
1067
+ : typeof item?.detail === "string" && item.detail.trim()
1068
+ ? item.detail.trim()
1069
+ : undefined;
1055
1070
  return {
1056
1071
  name: stringField(tool, "name") ??
1057
1072
  stringField(inner, "name") ??
1058
1073
  stringField(item, "name") ??
1074
+ inferDeepseekToolName(item) ??
1059
1075
  stringField(item, "type") ??
1060
1076
  "tool",
1061
1077
  params: parseMaybeJson(tool?.input ??
1062
1078
  tool?.rawInput ??
1063
1079
  tool?.arguments ??
1080
+ tool?.params ??
1064
1081
  inner.input ??
1082
+ inner.arguments ??
1083
+ inner.params ??
1065
1084
  item?.input ??
1066
- item?.arguments) ?? tool ?? item,
1085
+ item?.arguments) ?? detailParams ?? tool ?? item,
1067
1086
  id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
1068
1087
  status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
1069
1088
  };
@@ -1082,8 +1101,15 @@ function extractDeepseekToolResult(raw) {
1082
1101
  id: stringField(payload, "id"),
1083
1102
  };
1084
1103
  }
1085
- if (payload.event === "item.completed" || payload.event === "item.failed") {
1086
- const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1104
+ if (raw?.event === "item.completed" ||
1105
+ raw?.event === "item.failed" ||
1106
+ payload.event === "item.completed" ||
1107
+ payload.event === "item.failed") {
1108
+ const inner = raw?.event === "item.completed" || raw?.event === "item.failed"
1109
+ ? payload
1110
+ : payload.payload && typeof payload.payload === "object"
1111
+ ? payload.payload
1112
+ : {};
1087
1113
  const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1088
1114
  const result = item?.output ??
1089
1115
  item?.result ??
@@ -1097,7 +1123,10 @@ function extractDeepseekToolResult(raw) {
1097
1123
  item ??
1098
1124
  inner;
1099
1125
  return {
1100
- name: stringField(item, "name") ?? stringField(item, "type") ?? stringField(inner, "name"),
1126
+ name: stringField(item, "name") ??
1127
+ inferDeepseekToolName(item) ??
1128
+ stringField(inner, "name") ??
1129
+ stringField(item, "type"),
1101
1130
  result: stringifyToolResult(result),
1102
1131
  id: stringField(item, "id") ?? stringField(inner, "id"),
1103
1132
  };
@@ -1112,7 +1141,12 @@ function formatBlockDetails(raw) {
1112
1141
  : typeof r.message === "string" ? r.message
1113
1142
  : typeof r.summary === "string" ? r.summary
1114
1143
  : typeof r.label === "string" ? r.label
1115
- : "";
1144
+ : typeof r.payload?.delta === "string" ? r.payload.delta
1145
+ : typeof r.payload?.item?.detail === "string" ? r.payload.item.detail
1146
+ : typeof r.payload?.item?.summary === "string" ? r.payload.item.summary
1147
+ : typeof r.payload?.payload?.item?.detail === "string" ? r.payload.payload.item.detail
1148
+ : typeof r.payload?.payload?.item?.summary === "string" ? r.payload.payload.item.summary
1149
+ : "";
1116
1150
  if (direct)
1117
1151
  return direct;
1118
1152
  const contentText = extractContentText(r.content ?? r.message?.content ?? r.params?.update?.content);
@@ -1212,6 +1246,17 @@ function parseMaybeJson(value) {
1212
1246
  return value;
1213
1247
  }
1214
1248
  }
1249
+ function inferDeepseekToolName(item) {
1250
+ const candidates = [stringField(item, "summary"), stringField(item, "detail")];
1251
+ for (const candidate of candidates) {
1252
+ if (!candidate)
1253
+ continue;
1254
+ const match = candidate.match(/^([A-Za-z0-9_.:-]+)\s*(?:started|completed|failed|returned|:)/);
1255
+ if (match?.[1] && match[1] !== "tool_call")
1256
+ return match[1];
1257
+ }
1258
+ return undefined;
1259
+ }
1215
1260
  function isEmptyRecord(value) {
1216
1261
  return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1217
1262
  }
@@ -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
  /**