@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
package/src/gateway-control.ts
CHANGED
|
@@ -313,13 +313,17 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
313
313
|
if (!loginId) {
|
|
314
314
|
return badParams("upsert_gateway: wechat requires loginId");
|
|
315
315
|
}
|
|
316
|
-
const
|
|
317
|
-
if (
|
|
316
|
+
const resolved = sessions.resolve(loginId);
|
|
317
|
+
if (resolved.state !== "live") {
|
|
318
318
|
return {
|
|
319
319
|
ok: false,
|
|
320
|
-
error:
|
|
320
|
+
error:
|
|
321
|
+
resolved.state === "missing"
|
|
322
|
+
? { code: "login_missing", message: `wechat login session "${loginId}" not found` }
|
|
323
|
+
: { code: "login_expired", message: `wechat login session "${loginId}" expired` },
|
|
321
324
|
};
|
|
322
325
|
}
|
|
326
|
+
const session = resolved.session!;
|
|
323
327
|
if (session.provider !== "wechat") {
|
|
324
328
|
return badParams(`upsert_gateway: login session provider "${session.provider}" != "wechat"`);
|
|
325
329
|
}
|
|
@@ -347,13 +351,17 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
347
351
|
if (!loginId) {
|
|
348
352
|
return badParams("upsert_gateway: feishu requires loginId");
|
|
349
353
|
}
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
354
|
+
const resolved = sessions.resolve(loginId);
|
|
355
|
+
if (resolved.state !== "live") {
|
|
352
356
|
return {
|
|
353
357
|
ok: false,
|
|
354
|
-
error:
|
|
358
|
+
error:
|
|
359
|
+
resolved.state === "missing"
|
|
360
|
+
? { code: "login_missing", message: `feishu login session "${loginId}" not found` }
|
|
361
|
+
: { code: "login_expired", message: `feishu login session "${loginId}" expired` },
|
|
355
362
|
};
|
|
356
363
|
}
|
|
364
|
+
const session = resolved.session!;
|
|
357
365
|
if (session.provider !== "feishu") {
|
|
358
366
|
return badParams(`upsert_gateway: login session provider "${session.provider}" != "feishu"`);
|
|
359
367
|
}
|
|
@@ -869,13 +877,17 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
869
877
|
if (!params.accountId || typeof params.accountId !== "string") {
|
|
870
878
|
return badParams("gateway_recent_senders: accountId is required");
|
|
871
879
|
}
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
880
|
+
const resolved = sessions.resolve(params.loginId);
|
|
881
|
+
if (resolved.state !== "live") {
|
|
874
882
|
return {
|
|
875
883
|
ok: false,
|
|
876
|
-
error:
|
|
884
|
+
error:
|
|
885
|
+
resolved.state === "missing"
|
|
886
|
+
? { code: "login_missing", message: `wechat login session "${params.loginId}" not found` }
|
|
887
|
+
: { code: "login_expired", message: `wechat login session "${params.loginId}" expired` },
|
|
877
888
|
};
|
|
878
889
|
}
|
|
890
|
+
const session = resolved.session!;
|
|
879
891
|
if (session.provider !== "wechat") {
|
|
880
892
|
return badParams("gateway_recent_senders: provider does not match login session");
|
|
881
893
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,10 +18,12 @@ import {
|
|
|
18
18
|
} from "./config.js";
|
|
19
19
|
import {
|
|
20
20
|
ensureNoOtherDaemonFromPidFile,
|
|
21
|
+
findOtherDaemonProcesses,
|
|
21
22
|
pidAlive,
|
|
22
23
|
readPid,
|
|
23
24
|
removePidFile,
|
|
24
25
|
stopDaemonFromPidFileForRestart,
|
|
26
|
+
stopOtherDaemonProcessesForRestart,
|
|
25
27
|
writeCurrentPid,
|
|
26
28
|
} from "./daemon-singleton.js";
|
|
27
29
|
import { resolveBootAgents } from "./agent-discovery.js";
|
|
@@ -585,6 +587,7 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
585
587
|
if (process.env.BOTCORD_DAEMON_CHILD !== "1") {
|
|
586
588
|
await ensureUserAuthForStart(args);
|
|
587
589
|
await stopDaemonFromPidFileForRestart({ logger: log });
|
|
590
|
+
await stopOtherDaemonProcessesForRestart({ logger: log });
|
|
588
591
|
} else {
|
|
589
592
|
const existing = ensureNoOtherDaemonFromPidFile();
|
|
590
593
|
if (existing) {
|
|
@@ -603,7 +606,7 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
603
606
|
env: { ...process.env, BOTCORD_DAEMON_CHILD: "1" },
|
|
604
607
|
});
|
|
605
608
|
child.unref();
|
|
606
|
-
const deadline = Date.now() +
|
|
609
|
+
const deadline = Date.now() + 5_000;
|
|
607
610
|
let observed: number | null = null;
|
|
608
611
|
while (Date.now() < deadline) {
|
|
609
612
|
const p = readPid();
|
|
@@ -614,7 +617,7 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
614
617
|
await new Promise((r) => setTimeout(r, 50));
|
|
615
618
|
}
|
|
616
619
|
if (!observed) {
|
|
617
|
-
console.error(`daemon did not record pid within
|
|
620
|
+
console.error(`daemon did not record pid within 5000ms (expected child pid ${child.pid})`);
|
|
618
621
|
process.exit(1);
|
|
619
622
|
}
|
|
620
623
|
console.log(`daemon started (pid ${observed})`);
|
|
@@ -659,6 +662,7 @@ async function cmdStartCloud(_args: ParsedArgs): Promise<void> {
|
|
|
659
662
|
hubUrl: cloudConfig.hubUrl,
|
|
660
663
|
});
|
|
661
664
|
await stopDaemonFromPidFileForRestart({ logger: log });
|
|
665
|
+
await stopOtherDaemonProcessesForRestart({ logger: log });
|
|
662
666
|
writeCurrentPid();
|
|
663
667
|
|
|
664
668
|
// Cloud daemons always start with an empty in-memory config — every
|
|
@@ -755,6 +759,7 @@ async function cmdStatus(args: ParsedArgs): Promise<void> {
|
|
|
755
759
|
const file = readSnapshotFile();
|
|
756
760
|
const now = Date.now();
|
|
757
761
|
const snapshotAgeMs = file ? now - file.writtenAt : null;
|
|
762
|
+
const daemonProcesses = findOtherDaemonProcesses().filter((p) => p.pid !== pid);
|
|
758
763
|
|
|
759
764
|
if (args.flags.json === true) {
|
|
760
765
|
const payload = {
|
|
@@ -780,6 +785,7 @@ async function cmdStatus(args: ParsedArgs): Promise<void> {
|
|
|
780
785
|
snapshotWrittenAt: file?.writtenAt ?? null,
|
|
781
786
|
snapshotAgeMs,
|
|
782
787
|
snapshotPath: SNAPSHOT_PATH,
|
|
788
|
+
daemonProcesses,
|
|
783
789
|
};
|
|
784
790
|
console.log(JSON.stringify(payload, null, 2));
|
|
785
791
|
return;
|
|
@@ -793,6 +799,7 @@ async function cmdStatus(args: ParsedArgs): Promise<void> {
|
|
|
793
799
|
configPath,
|
|
794
800
|
snapshot: file?.snapshot ?? null,
|
|
795
801
|
snapshotAgeMs,
|
|
802
|
+
daemonProcesses,
|
|
796
803
|
};
|
|
797
804
|
console.log(renderStatus(input, now));
|
|
798
805
|
if (userAuth) {
|
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
|
+
}
|
package/src/status-render.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const STALE_THRESHOLD_MS = 30_000;
|
|
|
7
7
|
export interface StatusRenderInput {
|
|
8
8
|
pid: number | null;
|
|
9
9
|
alive: boolean;
|
|
10
|
+
daemonProcesses?: Array<{ pid: number; command?: string }> | null;
|
|
10
11
|
/**
|
|
11
12
|
* Effective list of agent ids the daemon is bound to. Single-agent installs
|
|
12
13
|
* show one entry; multi-agent configs show all. `agentId` (scalar) is kept
|
|
@@ -114,10 +115,22 @@ function renderRuntimeCircuitBreakers(
|
|
|
114
115
|
export function renderStatus(input: StatusRenderInput, now: number = Date.now()): string {
|
|
115
116
|
const lines: string[] = [];
|
|
116
117
|
if (input.pid === null) {
|
|
117
|
-
|
|
118
|
+
const extras = input.daemonProcesses ?? [];
|
|
119
|
+
if (extras.length > 0) {
|
|
120
|
+
lines.push("daemon: no pid file");
|
|
121
|
+
lines.push(`warning: ${extras.length} daemon process${extras.length === 1 ? "" : "es"} detected without pid file`);
|
|
122
|
+
lines.push(`pids: ${extras.map((p) => p.pid).join(", ")}`);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push("daemon: stopped");
|
|
125
|
+
}
|
|
118
126
|
return lines.join("\n");
|
|
119
127
|
}
|
|
120
128
|
lines.push(`daemon: pid ${input.pid} (${input.alive ? "alive" : "not alive"})`);
|
|
129
|
+
const extras = (input.daemonProcesses ?? []).filter((p) => p.pid !== input.pid);
|
|
130
|
+
if (extras.length > 0) {
|
|
131
|
+
lines.push(`warning: ${extras.length} additional daemon process${extras.length === 1 ? "" : "es"} detected`);
|
|
132
|
+
lines.push(`extra pids: ${extras.map((p) => p.pid).join(", ")}`);
|
|
133
|
+
}
|
|
121
134
|
const agents =
|
|
122
135
|
input.agents && input.agents.length > 0
|
|
123
136
|
? input.agents
|