@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
|
@@ -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
|
+
}
|
|
@@ -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;
|
package/dist/daemon-singleton.js
CHANGED
|
@@ -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 (
|
|
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,
|
|
@@ -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:{
|
|
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
|
-
|
|
861
|
-
|
|
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 =
|
|
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 (
|
|
1086
|
-
|
|
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") ??
|
|
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
|
/**
|