@botcord/daemon 0.2.78 → 0.2.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attention-policy-fetcher.d.ts +14 -0
- package/dist/attention-policy-fetcher.js +59 -0
- package/dist/cloud-daemon.js +8 -0
- package/dist/cloud-gateway-runtime.d.ts +29 -0
- package/dist/cloud-gateway-runtime.js +122 -0
- package/dist/daemon.js +21 -6
- package/dist/gateway/channels/botcord.js +29 -9
- package/dist/gateway/channels/login-session.d.ts +12 -0
- package/dist/gateway/channels/login-session.js +20 -2
- package/dist/gateway/channels/sanitize.d.ts +5 -18
- package/dist/gateway/channels/sanitize.js +5 -54
- package/dist/gateway/channels/text-split.d.ts +5 -11
- package/dist/gateway/channels/text-split.js +5 -31
- package/dist/gateway/dispatcher.d.ts +7 -1
- package/dist/gateway/dispatcher.js +88 -8
- package/dist/gateway/gateway.d.ts +16 -1
- package/dist/gateway/gateway.js +21 -0
- package/dist/gateway/policy-resolver.js +17 -9
- package/dist/gateway/runtimes/deepseek-tui.js +31 -12
- package/dist/gateway/types.d.ts +12 -57
- package/dist/gateway-control.js +18 -9
- package/dist/provision.d.ts +7 -3
- package/dist/provision.js +115 -8
- package/dist/room-recovery-context.d.ts +11 -0
- package/dist/room-recovery-context.js +97 -0
- package/package.json +2 -2
- package/src/__tests__/attention-policy-fetcher.test.ts +67 -0
- package/src/__tests__/cloud-gateway-runtime.test.ts +127 -0
- package/src/__tests__/gateway-control.test.ts +136 -0
- package/src/__tests__/policy-resolver.test.ts +20 -0
- package/src/__tests__/provision.test.ts +65 -0
- package/src/attention-policy-fetcher.ts +87 -0
- package/src/cloud-daemon.ts +8 -0
- package/src/cloud-gateway-runtime.ts +171 -0
- package/src/daemon.ts +23 -6
- package/src/gateway/__tests__/botcord-channel.test.ts +97 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +177 -0
- package/src/gateway/__tests__/dispatcher.test.ts +56 -0
- package/src/gateway/channels/botcord.ts +32 -8
- package/src/gateway/channels/login-session.ts +20 -2
- package/src/gateway/channels/sanitize.ts +8 -66
- package/src/gateway/channels/text-split.ts +5 -27
- package/src/gateway/dispatcher.ts +123 -27
- package/src/gateway/gateway.ts +29 -0
- package/src/gateway/policy-resolver.ts +20 -9
- package/src/gateway/runtimes/deepseek-tui.ts +37 -12
- package/src/gateway/types.ts +31 -59
- package/src/gateway-control.ts +21 -9
- package/src/provision.ts +133 -7
- package/src/room-recovery-context.ts +131 -0
package/src/provision.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type RuntimeProbeResult,
|
|
27
27
|
type StoredBotCordCredentials,
|
|
28
28
|
type UpdateAgentParams,
|
|
29
|
+
type GatewayInboundFrame,
|
|
29
30
|
} from "@botcord/protocol-core";
|
|
30
31
|
import type { Gateway } from "./gateway/index.js";
|
|
31
32
|
import type { GatewayInboundMessage } from "./gateway/index.js";
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
buildRuntimeSelectionExtraArgs,
|
|
78
79
|
mergeRuntimeExtraArgs,
|
|
79
80
|
} from "./runtime-route-options.js";
|
|
81
|
+
import { handleCloudGatewayRuntimeInbound } from "./cloud-gateway-runtime.js";
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
84
|
* Information passed to {@link OnAgentInstalledHook} after a successful
|
|
@@ -164,7 +166,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
164
166
|
|
|
165
167
|
case CONTROL_FRAME_TYPES.HELLO: {
|
|
166
168
|
const params = (frame.params ?? {}) as unknown as HelloParams;
|
|
167
|
-
const result = applyHelloIdentitySnapshot(params.agents);
|
|
169
|
+
const result = applyHelloIdentitySnapshot(params.agents, { gateway });
|
|
168
170
|
daemonLog.debug("hello: identity snapshot applied", {
|
|
169
171
|
frameId: frame.id,
|
|
170
172
|
received: params.agents?.length ?? 0,
|
|
@@ -186,12 +188,19 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
186
188
|
displayName: params.displayName,
|
|
187
189
|
bio: params.bio,
|
|
188
190
|
});
|
|
191
|
+
const runtimeResult = applyAgentRuntimeSnapshot(params, { gateway });
|
|
192
|
+
const combined = {
|
|
193
|
+
changed: result.changed || runtimeResult.changed,
|
|
194
|
+
identity: result,
|
|
195
|
+
runtime: runtimeResult,
|
|
196
|
+
};
|
|
189
197
|
daemonLog.info("update_agent applied", {
|
|
190
198
|
agentId: params.agentId,
|
|
191
|
-
changed:
|
|
192
|
-
|
|
199
|
+
changed: combined.changed,
|
|
200
|
+
identitySkipped: result.skipped ?? null,
|
|
201
|
+
runtimeSkipped: runtimeResult.skipped ?? null,
|
|
193
202
|
});
|
|
194
|
-
return { ok: true, result };
|
|
203
|
+
return { ok: true, result: combined };
|
|
195
204
|
}
|
|
196
205
|
|
|
197
206
|
case CONTROL_FRAME_TYPES.PROVISION_AGENT: {
|
|
@@ -419,6 +428,31 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
419
428
|
);
|
|
420
429
|
}
|
|
421
430
|
|
|
431
|
+
case "cloud_gateway_runtime_inbound": {
|
|
432
|
+
const params = (frame.params ?? {}) as { frame?: unknown };
|
|
433
|
+
const runtimeFrame = params.frame as GatewayInboundFrame | undefined;
|
|
434
|
+
if (!runtimeFrame || typeof runtimeFrame !== "object") {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
error: {
|
|
438
|
+
code: "bad_params",
|
|
439
|
+
message: "cloud_gateway_runtime_inbound requires params.frame",
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const result = await handleCloudGatewayRuntimeInbound(gateway, runtimeFrame);
|
|
444
|
+
return result.accepted
|
|
445
|
+
? { ok: true, result }
|
|
446
|
+
: {
|
|
447
|
+
ok: false,
|
|
448
|
+
result,
|
|
449
|
+
error: result.error ?? {
|
|
450
|
+
code: "runtime_inbound_rejected",
|
|
451
|
+
message: "cloud gateway runtime inbound was rejected",
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
422
456
|
case "list_agent_files": {
|
|
423
457
|
const params = (frame.params ?? {}) as unknown as ListAgentFilesParams;
|
|
424
458
|
if (!params.agentId) {
|
|
@@ -2410,10 +2444,100 @@ interface HelloIdentityResult {
|
|
|
2410
2444
|
skipped: number;
|
|
2411
2445
|
}
|
|
2412
2446
|
|
|
2447
|
+
interface RuntimeSnapshotResult {
|
|
2448
|
+
changed: boolean;
|
|
2449
|
+
skipped?: string;
|
|
2450
|
+
routeUpdated?: boolean;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
interface RuntimeSnapshotCtx {
|
|
2454
|
+
gateway?: Gateway;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function hasOwnField(obj: object, key: string): boolean {
|
|
2458
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function cleanNullableString(value: string | null | undefined): string | undefined {
|
|
2462
|
+
if (typeof value !== "string") return undefined;
|
|
2463
|
+
const trimmed = value.trim();
|
|
2464
|
+
return trimmed || undefined;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function applyAgentRuntimeSnapshot(
|
|
2468
|
+
snapshot: AgentIdentitySnapshot,
|
|
2469
|
+
ctx: RuntimeSnapshotCtx = {},
|
|
2470
|
+
): RuntimeSnapshotResult {
|
|
2471
|
+
const hasRuntimeFields = (
|
|
2472
|
+
hasOwnField(snapshot, "runtime") ||
|
|
2473
|
+
hasOwnField(snapshot, "runtimeModel") ||
|
|
2474
|
+
hasOwnField(snapshot, "reasoningEffort") ||
|
|
2475
|
+
hasOwnField(snapshot, "thinking")
|
|
2476
|
+
);
|
|
2477
|
+
if (!hasRuntimeFields) return { changed: false, skipped: "no_runtime_fields" };
|
|
2478
|
+
|
|
2479
|
+
const credentialsFile = defaultCredentialsFile(snapshot.agentId);
|
|
2480
|
+
if (!existsSync(credentialsFile)) {
|
|
2481
|
+
return { changed: false, skipped: "credentials_missing" };
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
const credentials = loadStoredCredentials(credentialsFile);
|
|
2485
|
+
let changed = false;
|
|
2486
|
+
|
|
2487
|
+
if (hasOwnField(snapshot, "runtime")) {
|
|
2488
|
+
const runtime = cleanNullableString(snapshot.runtime);
|
|
2489
|
+
if (runtime !== credentials.runtime) {
|
|
2490
|
+
if (runtime) credentials.runtime = runtime;
|
|
2491
|
+
else delete credentials.runtime;
|
|
2492
|
+
changed = true;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
if (hasOwnField(snapshot, "runtimeModel")) {
|
|
2497
|
+
const runtimeModel = cleanNullableString(snapshot.runtimeModel);
|
|
2498
|
+
if (runtimeModel !== credentials.runtimeModel) {
|
|
2499
|
+
if (runtimeModel) credentials.runtimeModel = runtimeModel;
|
|
2500
|
+
else delete credentials.runtimeModel;
|
|
2501
|
+
changed = true;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
if (hasOwnField(snapshot, "reasoningEffort")) {
|
|
2506
|
+
const reasoningEffort = cleanNullableString(snapshot.reasoningEffort);
|
|
2507
|
+
if (reasoningEffort !== credentials.reasoningEffort) {
|
|
2508
|
+
if (reasoningEffort) credentials.reasoningEffort = reasoningEffort;
|
|
2509
|
+
else delete credentials.reasoningEffort;
|
|
2510
|
+
changed = true;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (hasOwnField(snapshot, "thinking")) {
|
|
2515
|
+
if (typeof snapshot.thinking === "boolean") {
|
|
2516
|
+
if (credentials.thinking !== snapshot.thinking) {
|
|
2517
|
+
credentials.thinking = snapshot.thinking;
|
|
2518
|
+
changed = true;
|
|
2519
|
+
}
|
|
2520
|
+
} else if (typeof credentials.thinking === "boolean") {
|
|
2521
|
+
delete credentials.thinking;
|
|
2522
|
+
changed = true;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
if (!changed) return { changed: false };
|
|
2527
|
+
|
|
2528
|
+
writeCredentialsFile(credentialsFile, credentials);
|
|
2529
|
+
if (ctx.gateway) {
|
|
2530
|
+
upsertManagedRouteForCredentials(credentials, loadConfig(), ctx.gateway);
|
|
2531
|
+
return { changed: true, routeUpdated: true };
|
|
2532
|
+
}
|
|
2533
|
+
return { changed: true };
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2413
2536
|
/**
|
|
2414
2537
|
* Reconcile every agent identity carried by the `hello.agents` snapshot
|
|
2415
|
-
* against the on-disk `identity.md
|
|
2416
|
-
* file-system error for one agent never
|
|
2538
|
+
* against the on-disk `identity.md` and credentials runtime selectors.
|
|
2539
|
+
* Best-effort: a malformed entry or a file-system error for one agent never
|
|
2540
|
+
* aborts the rest.
|
|
2417
2541
|
*
|
|
2418
2542
|
* Identity-snapshot semantics intentionally only touch the metadata
|
|
2419
2543
|
* line + Bio body — Role/Boundaries paragraphs the user authored locally
|
|
@@ -2423,6 +2547,7 @@ interface HelloIdentityResult {
|
|
|
2423
2547
|
*/
|
|
2424
2548
|
export function applyHelloIdentitySnapshot(
|
|
2425
2549
|
snapshot: AgentIdentitySnapshot[] | undefined,
|
|
2550
|
+
ctx: RuntimeSnapshotCtx = {},
|
|
2426
2551
|
): HelloIdentityResult {
|
|
2427
2552
|
const out: HelloIdentityResult = { updated: 0, skipped: 0 };
|
|
2428
2553
|
if (!Array.isArray(snapshot)) return out;
|
|
@@ -2436,7 +2561,8 @@ export function applyHelloIdentitySnapshot(
|
|
|
2436
2561
|
displayName: entry.displayName,
|
|
2437
2562
|
bio: entry.bio,
|
|
2438
2563
|
});
|
|
2439
|
-
|
|
2564
|
+
const runtimeResult = applyAgentRuntimeSnapshot(entry, ctx);
|
|
2565
|
+
if (result.changed || runtimeResult.changed) out.updated += 1;
|
|
2440
2566
|
else out.skipped += 1;
|
|
2441
2567
|
} catch (err) {
|
|
2442
2568
|
out.skipped += 1;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a compact, deterministic recovery block from recent Hub room messages.
|
|
3
|
+
* Used when a runtime-native session is discarded and the same turn is retried
|
|
4
|
+
* in a fresh session.
|
|
5
|
+
*/
|
|
6
|
+
import { BotCordClient, loadStoredCredentials } from "@botcord/protocol-core";
|
|
7
|
+
import { sanitizeUntrustedContent } from "./gateway/index.js";
|
|
8
|
+
import type { GatewayInboundMessage } from "./gateway/index.js";
|
|
9
|
+
|
|
10
|
+
interface CachedClient {
|
|
11
|
+
client: BotCordClient;
|
|
12
|
+
credentialsPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RecentRoomMessagesRecoveryOptions {
|
|
16
|
+
credentialPathByAgentId: Map<string, string>;
|
|
17
|
+
defaultCredentialsPath?: string;
|
|
18
|
+
hubBaseUrl?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
log?: {
|
|
21
|
+
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface RoomMessage {
|
|
26
|
+
from?: string;
|
|
27
|
+
from_name?: string;
|
|
28
|
+
text?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
ts?: string;
|
|
31
|
+
topic_id?: string | null;
|
|
32
|
+
topic_title?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_RECENT_LIMIT = 20;
|
|
36
|
+
const MAX_MESSAGE_TEXT_CHARS = 1200;
|
|
37
|
+
|
|
38
|
+
function stripNewlines(s: string): string {
|
|
39
|
+
return s.replace(/[\r\n]+/g, " ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function messageLabel(m: RoomMessage): string {
|
|
43
|
+
const name = typeof m.from_name === "string" && m.from_name.trim()
|
|
44
|
+
? m.from_name
|
|
45
|
+
: typeof m.from === "string" && m.from.trim()
|
|
46
|
+
? m.from
|
|
47
|
+
: "unknown";
|
|
48
|
+
return sanitizeUntrustedContent(stripNewlines(name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatRecentMessages(messages: RoomMessage[]): string {
|
|
52
|
+
if (messages.length === 0) return "[Recent Room Messages]\n(none)";
|
|
53
|
+
const chronological = [...messages].reverse();
|
|
54
|
+
const lines = ["[Recent Room Messages]"];
|
|
55
|
+
for (const m of chronological) {
|
|
56
|
+
const text = typeof m.text === "string" ? m.text.trim() : "";
|
|
57
|
+
if (!text) continue;
|
|
58
|
+
const ts = typeof m.ts === "string" ? m.ts : "";
|
|
59
|
+
const topic = typeof m.topic_title === "string" && m.topic_title.trim()
|
|
60
|
+
? ` topic=${sanitizeUntrustedContent(stripNewlines(m.topic_title))}`
|
|
61
|
+
: typeof m.topic_id === "string" && m.topic_id
|
|
62
|
+
? ` topic=${sanitizeUntrustedContent(stripNewlines(m.topic_id))}`
|
|
63
|
+
: "";
|
|
64
|
+
const safeText = sanitizeUntrustedContent(
|
|
65
|
+
text.length > MAX_MESSAGE_TEXT_CHARS
|
|
66
|
+
? `${text.slice(0, MAX_MESSAGE_TEXT_CHARS)}...`
|
|
67
|
+
: text,
|
|
68
|
+
);
|
|
69
|
+
lines.push(`- ${ts ? `${ts} ` : ""}${messageLabel(m)}${topic}: ${safeText}`);
|
|
70
|
+
}
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createRecentRoomMessagesRecoveryBuilder(
|
|
75
|
+
opts: RecentRoomMessagesRecoveryOptions,
|
|
76
|
+
): (message: GatewayInboundMessage) => Promise<string | null> {
|
|
77
|
+
const clients = new Map<string, CachedClient>();
|
|
78
|
+
const limit = opts.limit ?? DEFAULT_RECENT_LIMIT;
|
|
79
|
+
|
|
80
|
+
function getClient(accountId: string): BotCordClient | null {
|
|
81
|
+
const existing = clients.get(accountId);
|
|
82
|
+
if (existing) return existing.client;
|
|
83
|
+
|
|
84
|
+
const credsPath =
|
|
85
|
+
opts.credentialPathByAgentId.get(accountId) ?? opts.defaultCredentialsPath;
|
|
86
|
+
if (!credsPath) {
|
|
87
|
+
opts.log?.warn("daemon.recovery-context.no-credentials", { accountId });
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const creds = loadStoredCredentials(credsPath);
|
|
93
|
+
const client = new BotCordClient({
|
|
94
|
+
hubUrl: opts.hubBaseUrl ?? creds.hubUrl,
|
|
95
|
+
agentId: creds.agentId,
|
|
96
|
+
keyId: creds.keyId,
|
|
97
|
+
privateKey: creds.privateKey,
|
|
98
|
+
...(creds.token ? { token: creds.token } : {}),
|
|
99
|
+
...(creds.tokenExpiresAt !== undefined
|
|
100
|
+
? { tokenExpiresAt: creds.tokenExpiresAt }
|
|
101
|
+
: {}),
|
|
102
|
+
});
|
|
103
|
+
clients.set(accountId, { client, credentialsPath: credsPath });
|
|
104
|
+
return client;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
opts.log?.warn("daemon.recovery-context.client-init-failed", {
|
|
107
|
+
accountId,
|
|
108
|
+
credsPath,
|
|
109
|
+
error: err instanceof Error ? err.message : String(err),
|
|
110
|
+
});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return async (message) => {
|
|
116
|
+
const client = getClient(message.accountId);
|
|
117
|
+
if (!client) return null;
|
|
118
|
+
try {
|
|
119
|
+
const body = await client.roomMessages(message.conversation.id, { limit });
|
|
120
|
+
const messages = Array.isArray(body?.messages) ? body.messages as RoomMessage[] : [];
|
|
121
|
+
return formatRecentMessages(messages);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
opts.log?.warn("daemon.recovery-context.fetch-failed", {
|
|
124
|
+
accountId: message.accountId,
|
|
125
|
+
roomId: message.conversation.id,
|
|
126
|
+
error: err instanceof Error ? err.message : String(err),
|
|
127
|
+
});
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|