@botcord/daemon 0.2.89 → 0.2.91
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/cloud-daemon.js +18 -4
- package/dist/daemon.d.ts +2 -3
- package/dist/daemon.js +21 -6
- package/dist/gateway/channels/botcord.js +35 -22
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +103 -9
- package/dist/runtime-models.js +46 -0
- package/dist/self-restart.d.ts +29 -0
- package/dist/self-restart.js +172 -0
- package/dist/skill-index.d.ts +14 -2
- package/dist/skill-index.js +109 -41
- package/dist/skill-installer.d.ts +61 -0
- package/dist/skill-installer.js +340 -0
- package/dist/system-context.d.ts +6 -0
- package/dist/system-context.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/provision.test.ts +23 -0
- package/src/__tests__/runtime-discovery.test.ts +6 -3
- package/src/__tests__/runtime-models.test.ts +53 -0
- package/src/__tests__/self-restart.test.ts +57 -0
- package/src/__tests__/skill-index.test.ts +130 -13
- package/src/__tests__/skill-installer.test.ts +224 -0
- package/src/cloud-daemon.ts +17 -4
- package/src/daemon.ts +23 -8
- package/src/gateway/__tests__/botcord-channel.test.ts +38 -0
- package/src/gateway/channels/botcord.ts +41 -22
- package/src/provision.ts +111 -14
- package/src/runtime-models.ts +47 -0
- package/src/self-restart.ts +218 -0
- package/src/skill-index.ts +130 -53
- package/src/skill-installer.ts +472 -0
- package/src/system-context.ts +7 -1
package/src/daemon.ts
CHANGED
|
@@ -52,7 +52,7 @@ import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-res
|
|
|
52
52
|
import { scanMention } from "./mention-scan.js";
|
|
53
53
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
54
54
|
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
55
|
-
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
55
|
+
import { collectAgentSkillSnapshot, type SkillIndexOptions } from "./skill-index.js";
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
@@ -249,7 +249,7 @@ export function pushRuntimeSnapshot(
|
|
|
249
249
|
export function pushAgentSkillSnapshot(
|
|
250
250
|
sink: RuntimeSnapshotSink,
|
|
251
251
|
agentId: string,
|
|
252
|
-
opts:
|
|
252
|
+
opts: SkillIndexOptions = {},
|
|
253
253
|
): boolean {
|
|
254
254
|
const snap = collectAgentSkillSnapshot(agentId, opts);
|
|
255
255
|
const ok = sink.send({
|
|
@@ -354,6 +354,13 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
354
354
|
);
|
|
355
355
|
|
|
356
356
|
const gwConfig = toGatewayConfig(opts.config, { agentIds, agentRuntimes });
|
|
357
|
+
const skillIndexOptionsForAgent = (agentId: string): SkillIndexOptions => {
|
|
358
|
+
const meta = agentRuntimes[agentId];
|
|
359
|
+
return {
|
|
360
|
+
runtime: meta?.runtime ?? opts.config.defaultRoute.adapter,
|
|
361
|
+
...(meta?.hermesProfile ? { hermesProfile: meta.hermesProfile } : {}),
|
|
362
|
+
};
|
|
363
|
+
};
|
|
357
364
|
|
|
358
365
|
// Per-agent hub URL — read from each credential file at boot. Used to
|
|
359
366
|
// populate `BOTCORD_HUB` for runtime CLI subprocesses so the bundled
|
|
@@ -420,6 +427,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
420
427
|
activityTracker,
|
|
421
428
|
roomContextBuilder,
|
|
422
429
|
loopRiskBuilder,
|
|
430
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(aid),
|
|
423
431
|
}),
|
|
424
432
|
);
|
|
425
433
|
}
|
|
@@ -530,11 +538,16 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
530
538
|
// next room-context fetch re-loads the BotCordClient against the new
|
|
531
539
|
// credential file.
|
|
532
540
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
533
|
-
if (info.runtime) {
|
|
534
|
-
|
|
541
|
+
if (info.runtime || info.hermesProfile) {
|
|
542
|
+
const next = {
|
|
535
543
|
...(agentRuntimes[info.agentId] ?? {}),
|
|
536
|
-
runtime: info.runtime,
|
|
544
|
+
...(info.runtime ? { runtime: info.runtime } : {}),
|
|
545
|
+
...(info.hermesProfile ? { hermesProfile: info.hermesProfile } : {}),
|
|
537
546
|
};
|
|
547
|
+
if (info.runtime && !info.hermesProfile) {
|
|
548
|
+
delete next.hermesProfile;
|
|
549
|
+
}
|
|
550
|
+
agentRuntimes[info.agentId] = next;
|
|
538
551
|
}
|
|
539
552
|
if (info.hubUrl) hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
540
553
|
if (info.displayName) displayNameByAgent.set(info.agentId, info.displayName);
|
|
@@ -546,6 +559,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
546
559
|
activityTracker,
|
|
547
560
|
roomContextBuilder,
|
|
548
561
|
loopRiskBuilder,
|
|
562
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
549
563
|
}),
|
|
550
564
|
);
|
|
551
565
|
}
|
|
@@ -677,11 +691,12 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
677
691
|
ok: pushed,
|
|
678
692
|
});
|
|
679
693
|
for (const agentId of agentIds) {
|
|
680
|
-
const
|
|
681
|
-
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
694
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
695
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
682
696
|
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
683
697
|
agentId,
|
|
684
|
-
runtime,
|
|
698
|
+
runtime: skillIndexOptions.runtime,
|
|
699
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
685
700
|
ok: skillsPushed,
|
|
686
701
|
});
|
|
687
702
|
}
|
|
@@ -1347,6 +1347,44 @@ describe("createBotCordChannel — typing()", () => {
|
|
|
1347
1347
|
globalThis.fetch = realFetch;
|
|
1348
1348
|
}
|
|
1349
1349
|
});
|
|
1350
|
+
|
|
1351
|
+
it("refreshes the token and retries once on 401 (stale-token recovery)", async () => {
|
|
1352
|
+
const fetchSpy = vi
|
|
1353
|
+
.fn()
|
|
1354
|
+
.mockResolvedValueOnce(new Response('{"code":"invalid_token"}', { status: 401 }))
|
|
1355
|
+
.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
1356
|
+
const realFetch = globalThis.fetch;
|
|
1357
|
+
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
|
1358
|
+
try {
|
|
1359
|
+
const client = makeClient({
|
|
1360
|
+
getHubUrl: vi.fn().mockReturnValue("https://hub.example.com"),
|
|
1361
|
+
});
|
|
1362
|
+
const channel = createBotCordChannel({
|
|
1363
|
+
id: "botcord-main",
|
|
1364
|
+
accountId: "ag_self",
|
|
1365
|
+
agentId: "ag_self",
|
|
1366
|
+
client,
|
|
1367
|
+
hubBaseUrl: "https://hub.example.com",
|
|
1368
|
+
});
|
|
1369
|
+
await channel.typing!({
|
|
1370
|
+
traceId: "trace_401",
|
|
1371
|
+
accountId: "ag_self",
|
|
1372
|
+
conversationId: "rm_oc_42",
|
|
1373
|
+
log: silentLog,
|
|
1374
|
+
});
|
|
1375
|
+
expect(client.refreshToken).toHaveBeenCalledTimes(1);
|
|
1376
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
1377
|
+
// First attempt uses the stale token; the retry uses the refreshed one.
|
|
1378
|
+
expect((fetchSpy.mock.calls[0][1].headers as Record<string, string>).Authorization).toBe(
|
|
1379
|
+
"Bearer test-token",
|
|
1380
|
+
);
|
|
1381
|
+
expect((fetchSpy.mock.calls[1][1].headers as Record<string, string>).Authorization).toBe(
|
|
1382
|
+
"Bearer test-token-2",
|
|
1383
|
+
);
|
|
1384
|
+
} finally {
|
|
1385
|
+
globalThis.fetch = realFetch;
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1350
1388
|
});
|
|
1351
1389
|
|
|
1352
1390
|
describe("createBotCordChannel — websocket logging", () => {
|
|
@@ -308,6 +308,41 @@ function normalizeInboxBatch(
|
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Fire-and-forget authenticated POST for presence/streaming control requests
|
|
313
|
+
* (`/hub/typing`, `/hub/stream-block`). Mirrors `BotCordClient.hubFetch`'s 401
|
|
314
|
+
* handling: a stale-but-unexpired token (e.g. after a Hub JWT secret rotation,
|
|
315
|
+
* which `ensureToken()` won't refresh because it only refreshes near expiry) is
|
|
316
|
+
* refreshed once and the request retried. Without this, typing/stream-block
|
|
317
|
+
* silently 401 in a loop until the next actual message send happens to refresh
|
|
318
|
+
* the token — leaving the conversation with no typing indicator or live stream.
|
|
319
|
+
*/
|
|
320
|
+
async function postControlWithRefresh(
|
|
321
|
+
client: BotCordChannelClient,
|
|
322
|
+
hubUrl: string,
|
|
323
|
+
path: string,
|
|
324
|
+
body: unknown,
|
|
325
|
+
): Promise<Response> {
|
|
326
|
+
let token = await client.ensureToken();
|
|
327
|
+
for (let attempt = 0; attempt <= 1; attempt++) {
|
|
328
|
+
const resp = await fetch(`${hubUrl}${path}`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: {
|
|
331
|
+
"Content-Type": "application/json",
|
|
332
|
+
Authorization: `Bearer ${token}`,
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify(body),
|
|
335
|
+
signal: AbortSignal.timeout(10_000),
|
|
336
|
+
});
|
|
337
|
+
if (resp.status === 401 && attempt === 0) {
|
|
338
|
+
token = await client.refreshToken();
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
return resp;
|
|
342
|
+
}
|
|
343
|
+
throw new Error("postControlWithRefresh: exhausted retries");
|
|
344
|
+
}
|
|
345
|
+
|
|
311
346
|
/**
|
|
312
347
|
* Construct a BotCord channel adapter.
|
|
313
348
|
*
|
|
@@ -895,21 +930,12 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
895
930
|
const client = ensureClient();
|
|
896
931
|
const hubUrl = options.hubBaseUrl ?? client.getHubUrl();
|
|
897
932
|
try {
|
|
898
|
-
const token = await client.ensureToken();
|
|
899
933
|
const block = ctx.block as { raw?: unknown; kind?: string; seq?: number } | undefined;
|
|
900
934
|
const seq = typeof block?.seq === "number" ? block.seq : 0;
|
|
901
|
-
const resp = await
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
Authorization: `Bearer ${token}`,
|
|
906
|
-
},
|
|
907
|
-
body: JSON.stringify({
|
|
908
|
-
trace_id: ctx.traceId,
|
|
909
|
-
seq,
|
|
910
|
-
block: normalizeBlockForHub(block, seq),
|
|
911
|
-
}),
|
|
912
|
-
signal: AbortSignal.timeout(10_000),
|
|
935
|
+
const resp = await postControlWithRefresh(client, hubUrl, "/hub/stream-block", {
|
|
936
|
+
trace_id: ctx.traceId,
|
|
937
|
+
seq,
|
|
938
|
+
block: normalizeBlockForHub(block, seq),
|
|
913
939
|
});
|
|
914
940
|
if (!resp.ok && resp.status !== 204) {
|
|
915
941
|
const body = await resp.text().catch(() => "");
|
|
@@ -927,15 +953,8 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
|
|
|
927
953
|
const client = ensureClient();
|
|
928
954
|
const hubUrl = options.hubBaseUrl ?? client.getHubUrl();
|
|
929
955
|
try {
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
method: "POST",
|
|
933
|
-
headers: {
|
|
934
|
-
"Content-Type": "application/json",
|
|
935
|
-
Authorization: `Bearer ${token}`,
|
|
936
|
-
},
|
|
937
|
-
body: JSON.stringify({ room_id: ctx.conversationId }),
|
|
938
|
-
signal: AbortSignal.timeout(10_000),
|
|
956
|
+
const resp = await postControlWithRefresh(client, hubUrl, "/hub/typing", {
|
|
957
|
+
room_id: ctx.conversationId,
|
|
939
958
|
});
|
|
940
959
|
if (!resp.ok && resp.status !== 204) {
|
|
941
960
|
const body = await resp.text().catch(() => "");
|
package/src/provision.ts
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
type StoredBotCordCredentials,
|
|
29
29
|
type UpdateAgentParams,
|
|
30
30
|
type GatewayInboundFrame,
|
|
31
|
+
type InstallAgentSkillParams,
|
|
32
|
+
type ListAgentSkillsParams,
|
|
31
33
|
} from "@botcord/protocol-core";
|
|
32
34
|
import type { Gateway } from "./gateway/index.js";
|
|
33
35
|
import type { GatewayInboundMessage } from "./gateway/index.js";
|
|
@@ -75,7 +77,12 @@ import { log as daemonLog } from "./log.js";
|
|
|
75
77
|
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
76
78
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
77
79
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
78
|
-
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
80
|
+
import { collectAgentSkillSnapshot, type SkillIndexOptions } from "./skill-index.js";
|
|
81
|
+
import {
|
|
82
|
+
installAgentSkillManifest,
|
|
83
|
+
installBotLearnArchiveManifest,
|
|
84
|
+
installVercelSkillsForAgent,
|
|
85
|
+
} from "./skill-installer.js";
|
|
79
86
|
import {
|
|
80
87
|
buildRuntimeSelectionExtraArgs,
|
|
81
88
|
mergeRuntimeExtraArgs,
|
|
@@ -84,15 +91,23 @@ import {
|
|
|
84
91
|
handleCloudGatewayRuntimeInbound,
|
|
85
92
|
type CloudGatewayTypingEmitter,
|
|
86
93
|
} from "./cloud-gateway-runtime.js";
|
|
94
|
+
import { scheduleDaemonSelfRestart } from "./self-restart.js";
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
function skillIndexOptionsForLoadedAgent(gateway: Gateway, agentId: string): SkillIndexOptions {
|
|
97
|
+
const route = gateway.listManagedRoutes()
|
|
98
|
+
.find((entry) => entry.match?.accountId === agentId);
|
|
99
|
+
let credentials: StoredBotCordCredentials | null = null;
|
|
100
|
+
try {
|
|
101
|
+
credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
|
|
102
|
+
} catch {
|
|
103
|
+
credentials = null;
|
|
104
|
+
}
|
|
105
|
+
const runtime = route?.runtime ?? credentials?.runtime;
|
|
106
|
+
const hermesProfile = route?.hermesProfile ?? credentials?.hermesProfile;
|
|
107
|
+
return {
|
|
108
|
+
...(runtime ? { runtime } : {}),
|
|
109
|
+
...(hermesProfile ? { hermesProfile } : {}),
|
|
110
|
+
};
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
/**
|
|
@@ -108,6 +123,7 @@ export interface InstalledAgentInfo {
|
|
|
108
123
|
hubUrl: string;
|
|
109
124
|
displayName?: string;
|
|
110
125
|
runtime?: string;
|
|
126
|
+
hermesProfile?: string;
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
/**
|
|
@@ -382,6 +398,17 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
382
398
|
return { ok: true, result: snapshot };
|
|
383
399
|
}
|
|
384
400
|
|
|
401
|
+
case CONTROL_FRAME_TYPES.RESTART_DAEMON: {
|
|
402
|
+
const plan = scheduleDaemonSelfRestart({ update: true });
|
|
403
|
+
daemonLog.warn("restart_daemon: scheduled self restart", {
|
|
404
|
+
updateRequested: plan.updateRequested,
|
|
405
|
+
updateSupported: plan.updateSupported,
|
|
406
|
+
installPrefix: plan.installPrefix,
|
|
407
|
+
packageSpec: plan.packageSpec,
|
|
408
|
+
});
|
|
409
|
+
return { ok: true, result: plan };
|
|
410
|
+
}
|
|
411
|
+
|
|
385
412
|
case "list_gateways":
|
|
386
413
|
return gatewayControl.handleList();
|
|
387
414
|
|
|
@@ -516,16 +543,77 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
516
543
|
},
|
|
517
544
|
};
|
|
518
545
|
}
|
|
519
|
-
const
|
|
520
|
-
const result = collectAgentSkillSnapshot(params.agentId,
|
|
546
|
+
const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
|
|
547
|
+
const result = collectAgentSkillSnapshot(params.agentId, skillIndexOptions);
|
|
521
548
|
daemonLog.debug("list_agent_skills", {
|
|
522
549
|
agentId: params.agentId,
|
|
523
|
-
runtime,
|
|
550
|
+
runtime: skillIndexOptions.runtime,
|
|
551
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
524
552
|
count: result.skills.length,
|
|
525
553
|
});
|
|
526
554
|
return { ok: true, result };
|
|
527
555
|
}
|
|
528
556
|
|
|
557
|
+
case CONTROL_FRAME_TYPES.INSTALL_AGENT_SKILL: {
|
|
558
|
+
const params = (frame.params ?? {}) as unknown as InstallAgentSkillParams;
|
|
559
|
+
if (!params.agentId) {
|
|
560
|
+
return {
|
|
561
|
+
ok: false,
|
|
562
|
+
error: { code: "bad_params", message: "install_agent_skill requires params.agentId" },
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const channels = gateway.snapshot().channels;
|
|
566
|
+
if (!channels[params.agentId]) {
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
error: {
|
|
570
|
+
code: "agent_not_loaded",
|
|
571
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
|
|
576
|
+
const runtime = skillIndexOptions.runtime;
|
|
577
|
+
const modes = [params.manifest, params.archiveManifest, params.vercel].filter(Boolean).length;
|
|
578
|
+
if (modes !== 1) {
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
error: {
|
|
582
|
+
code: "bad_params",
|
|
583
|
+
message: "install_agent_skill requires exactly one of manifest, archiveManifest, or vercel",
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
let result;
|
|
588
|
+
try {
|
|
589
|
+
result = params.vercel
|
|
590
|
+
? await installVercelSkillsForAgent({
|
|
591
|
+
agentId: params.agentId,
|
|
592
|
+
packageSpec: params.vercel.packageSpec,
|
|
593
|
+
skills: params.vercel.skills,
|
|
594
|
+
runtime,
|
|
595
|
+
})
|
|
596
|
+
: params.archiveManifest
|
|
597
|
+
? installBotLearnArchiveManifest(params.agentId, params.archiveManifest, { runtime })
|
|
598
|
+
: installAgentSkillManifest(params.agentId, params.manifest!, { runtime });
|
|
599
|
+
} catch (err) {
|
|
600
|
+
return {
|
|
601
|
+
ok: false,
|
|
602
|
+
error: {
|
|
603
|
+
code: "skill_install_failed",
|
|
604
|
+
message: err instanceof Error ? err.message : String(err),
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
daemonLog.debug("install_agent_skill", {
|
|
609
|
+
agentId: params.agentId,
|
|
610
|
+
runtime,
|
|
611
|
+
installed: result.installed.map((s) => s.name),
|
|
612
|
+
snapshotCount: result.snapshot.skills.length,
|
|
613
|
+
});
|
|
614
|
+
return { ok: true, result };
|
|
615
|
+
}
|
|
616
|
+
|
|
529
617
|
case "wake_agent": {
|
|
530
618
|
return handleWakeAgent(gateway, frame.params);
|
|
531
619
|
}
|
|
@@ -653,8 +741,15 @@ async function handleWakeAgent(gateway: Gateway, raw: unknown): Promise<AckBody>
|
|
|
653
741
|
},
|
|
654
742
|
};
|
|
655
743
|
|
|
656
|
-
|
|
657
|
-
|
|
744
|
+
void gateway.injectInbound(msg).catch((err) => {
|
|
745
|
+
daemonLog.error("wake_agent: async inject failed", {
|
|
746
|
+
agentId,
|
|
747
|
+
scheduleId: scheduleId ?? null,
|
|
748
|
+
runId,
|
|
749
|
+
error: err instanceof Error ? err.message : String(err),
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
return { ok: true, result: { agent_id: agentId, queued: true } };
|
|
658
753
|
}
|
|
659
754
|
|
|
660
755
|
// W8: hand-written runtime validator for the third-party gateway frame
|
|
@@ -1118,6 +1213,7 @@ async function installLocalAgent(
|
|
|
1118
1213
|
hubUrl: credentials.hubUrl,
|
|
1119
1214
|
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
1120
1215
|
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
1216
|
+
...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
|
|
1121
1217
|
});
|
|
1122
1218
|
} catch (err) {
|
|
1123
1219
|
// Hook misbehavior must not fail the install — the agent is already
|
|
@@ -1212,6 +1308,7 @@ async function installExistingOpenclawBinding(
|
|
|
1212
1308
|
hubUrl: credentials.hubUrl,
|
|
1213
1309
|
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
1214
1310
|
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
1311
|
+
...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
|
|
1215
1312
|
});
|
|
1216
1313
|
} catch (err) {
|
|
1217
1314
|
daemonLog.error("provision.onAgentInstalled threw — caches may be stale", {
|
package/src/runtime-models.ts
CHANGED
|
@@ -68,6 +68,37 @@ const KIMI_FALLBACK_MODELS: RuntimeModelProbe[] = [
|
|
|
68
68
|
{ id: "kimi-k2-0711", displayName: "kimi-k2-0711", provider: "kimi", source: "builtin" },
|
|
69
69
|
];
|
|
70
70
|
|
|
71
|
+
// Gemini CLI has no `gemini models list` command — the model set is
|
|
72
|
+
// hard-coded inside the bundle's `VALID_GEMINI_MODELS` (see `@google/gemini-cli`
|
|
73
|
+
// bundle `chunk-BE42OOYM.js:275832`). We mirror the documented user-facing
|
|
74
|
+
// names here as a static catalog; the actual availability depends on the
|
|
75
|
+
// user's auth tier (gcloud ADC / Vertex / GEMINI_API_KEY) and project quota.
|
|
76
|
+
// The `auto` alias is the default — it lets gemini pick the best model.
|
|
77
|
+
const GEMINI_FALLBACK_MODELS: RuntimeModelProbe[] = [
|
|
78
|
+
{ id: "auto", displayName: "Auto (let Gemini pick)", provider: "google", source: "builtin", isDefault: true },
|
|
79
|
+
{ id: "pro", displayName: "Pro alias", provider: "google", source: "builtin" },
|
|
80
|
+
{ id: "flash", displayName: "Flash alias", provider: "google", source: "builtin" },
|
|
81
|
+
{ id: "flash-lite", displayName: "Flash Lite alias", provider: "google", source: "builtin" },
|
|
82
|
+
{ id: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", provider: "google", source: "builtin" },
|
|
83
|
+
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", provider: "google", source: "builtin" },
|
|
84
|
+
{ id: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", provider: "google", source: "builtin" },
|
|
85
|
+
{ id: "gemini-3-pro-preview", displayName: "Gemini 3 Pro (preview)", provider: "google", source: "builtin" },
|
|
86
|
+
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (preview)", provider: "google", source: "builtin" },
|
|
87
|
+
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (preview)", provider: "google", source: "builtin" },
|
|
88
|
+
{
|
|
89
|
+
id: "gemini-3.1-pro-preview-customtools",
|
|
90
|
+
displayName: "Gemini 3.1 Pro Custom Tools (preview)",
|
|
91
|
+
provider: "google",
|
|
92
|
+
source: "builtin",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "gemini-3.1-flash-lite-preview",
|
|
96
|
+
displayName: "Gemini 3.1 Flash Lite (preview)",
|
|
97
|
+
provider: "google",
|
|
98
|
+
source: "builtin",
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
71
102
|
export interface RuntimeModelDiscovery {
|
|
72
103
|
models?: RuntimeModelProbe[];
|
|
73
104
|
parameters?: RuntimeParameterProbe[];
|
|
@@ -160,6 +191,22 @@ function runtimeCatalogStrategy(entry: RuntimeProbeEntry): RuntimeCatalogStrateg
|
|
|
160
191
|
],
|
|
161
192
|
}),
|
|
162
193
|
};
|
|
194
|
+
case "gemini":
|
|
195
|
+
// Gemini CLI exposes no runtime discovery command and its thinking
|
|
196
|
+
// budget / level (see bundle `ThinkingLevel`, `thinkingBudget`) is
|
|
197
|
+
// configured per-installation in `~/.gemini/settings.json`, not via
|
|
198
|
+
// any CLI flag — so we ship a static model catalog with no parameter
|
|
199
|
+
// controls. settings.json + BOTCORD_GEMINI_BIN feed the cache key so
|
|
200
|
+
// user-side reconfig (e.g. switching auth type) busts the cache.
|
|
201
|
+
return {
|
|
202
|
+
id: entry.id,
|
|
203
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
204
|
+
settings: fileStatKey(path.join(homedir(), ".gemini", "settings.json")),
|
|
205
|
+
env: pickEnv(["BOTCORD_GEMINI_BIN", "GEMINI_CLI_HOME"]),
|
|
206
|
+
}),
|
|
207
|
+
discoverFresh: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
|
|
208
|
+
fallback: () => ({ models: GEMINI_FALLBACK_MODELS.slice() }),
|
|
209
|
+
};
|
|
163
210
|
default:
|
|
164
211
|
return null;
|
|
165
212
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DAEMON_PACKAGE = "@botcord/daemon@latest";
|
|
6
|
+
const DEFAULT_SHUTDOWN_DELAY_MS = 750;
|
|
7
|
+
const DEFAULT_FORCE_EXIT_MS = 10_000;
|
|
8
|
+
const DEFAULT_PARENT_EXIT_WAIT_MS = 30_000;
|
|
9
|
+
const DEFAULT_INSTALL_TIMEOUT_MS = 120_000;
|
|
10
|
+
|
|
11
|
+
export interface DaemonRestartPlan {
|
|
12
|
+
scheduled: boolean;
|
|
13
|
+
updateRequested: boolean;
|
|
14
|
+
updateSupported: boolean;
|
|
15
|
+
installPrefix: string | null;
|
|
16
|
+
packageSpec: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScheduleDaemonSelfRestartOptions {
|
|
20
|
+
update?: boolean;
|
|
21
|
+
packageSpec?: string;
|
|
22
|
+
delayMs?: number;
|
|
23
|
+
forceExitAfterMs?: number;
|
|
24
|
+
entrypoint?: string;
|
|
25
|
+
restartArgs?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ScheduleDaemonSelfRestartDeps {
|
|
29
|
+
spawn?: typeof spawn;
|
|
30
|
+
setTimeout?: typeof setTimeout;
|
|
31
|
+
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
32
|
+
exit?: (code?: number) => never;
|
|
33
|
+
pid?: number;
|
|
34
|
+
execPath?: string;
|
|
35
|
+
argv?: string[];
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findDaemonInstallPrefix(entrypoint?: string): string | null {
|
|
40
|
+
if (!entrypoint) return null;
|
|
41
|
+
const candidates = [entrypoint, safeRealpath(entrypoint)];
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
if (!candidate) continue;
|
|
44
|
+
const prefix = installPrefixFromPath(candidate);
|
|
45
|
+
if (prefix) return prefix;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveNpmBin(nodePath = process.execPath): string {
|
|
51
|
+
const name = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
52
|
+
const sibling = path.join(path.dirname(nodePath), name);
|
|
53
|
+
return existsSync(sibling) ? sibling : name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function scheduleDaemonSelfRestart(
|
|
57
|
+
opts: ScheduleDaemonSelfRestartOptions = {},
|
|
58
|
+
deps: ScheduleDaemonSelfRestartDeps = {},
|
|
59
|
+
): DaemonRestartPlan {
|
|
60
|
+
const env = deps.env ?? process.env;
|
|
61
|
+
const argv = deps.argv ?? process.argv;
|
|
62
|
+
const entrypoint = opts.entrypoint ?? argv[1];
|
|
63
|
+
if (!entrypoint) {
|
|
64
|
+
throw new Error("cannot restart daemon: process entrypoint is unknown");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const updateRequested = opts.update !== false;
|
|
68
|
+
const installPrefix = updateRequested ? findDaemonInstallPrefix(entrypoint) : null;
|
|
69
|
+
const packageSpec = opts.packageSpec ?? env.BOTCORD_DAEMON_PACKAGE ?? DEFAULT_DAEMON_PACKAGE;
|
|
70
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
71
|
+
const pid = deps.pid ?? process.pid;
|
|
72
|
+
const restartArgs = opts.restartArgs ?? ["start", "--foreground"];
|
|
73
|
+
const supervisorEnv: NodeJS.ProcessEnv = {
|
|
74
|
+
...env,
|
|
75
|
+
BOTCORD_DAEMON_CHILD: "1",
|
|
76
|
+
BOTCORD_RESTART_PARENT_PID: String(pid),
|
|
77
|
+
BOTCORD_RESTART_ENTRYPOINT: entrypoint,
|
|
78
|
+
BOTCORD_RESTART_ARGS_JSON: JSON.stringify(restartArgs),
|
|
79
|
+
BOTCORD_RESTART_NODE: execPath,
|
|
80
|
+
BOTCORD_RESTART_NPM_BIN: resolveNpmBin(execPath),
|
|
81
|
+
BOTCORD_RESTART_UPDATE: updateRequested && installPrefix ? "1" : "0",
|
|
82
|
+
BOTCORD_RESTART_INSTALL_PREFIX: installPrefix ?? "",
|
|
83
|
+
BOTCORD_RESTART_PACKAGE: packageSpec,
|
|
84
|
+
BOTCORD_RESTART_PARENT_EXIT_WAIT_MS: String(DEFAULT_PARENT_EXIT_WAIT_MS),
|
|
85
|
+
BOTCORD_RESTART_INSTALL_TIMEOUT_MS: String(DEFAULT_INSTALL_TIMEOUT_MS),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const spawnImpl = deps.spawn ?? spawn;
|
|
89
|
+
const child = spawnImpl(execPath, ["-e", RESTART_SUPERVISOR_SCRIPT], {
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: "ignore",
|
|
92
|
+
env: supervisorEnv,
|
|
93
|
+
}) as ChildProcess;
|
|
94
|
+
child.unref();
|
|
95
|
+
|
|
96
|
+
const setTimer = deps.setTimeout ?? setTimeout;
|
|
97
|
+
const kill = deps.kill ?? process.kill.bind(process);
|
|
98
|
+
const exit = deps.exit ?? process.exit.bind(process);
|
|
99
|
+
const delayMs = opts.delayMs ?? DEFAULT_SHUTDOWN_DELAY_MS;
|
|
100
|
+
const forceExitAfterMs = opts.forceExitAfterMs ?? DEFAULT_FORCE_EXIT_MS;
|
|
101
|
+
const shutdownTimer = setTimer(() => {
|
|
102
|
+
try {
|
|
103
|
+
kill(pid, "SIGTERM");
|
|
104
|
+
} catch {
|
|
105
|
+
exit(0);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const exitTimer = setTimer(() => exit(0), forceExitAfterMs);
|
|
109
|
+
unrefTimer(exitTimer);
|
|
110
|
+
}, delayMs);
|
|
111
|
+
unrefTimer(shutdownTimer);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
scheduled: true,
|
|
115
|
+
updateRequested,
|
|
116
|
+
updateSupported: installPrefix !== null,
|
|
117
|
+
installPrefix,
|
|
118
|
+
packageSpec,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function safeRealpath(input: string): string | null {
|
|
123
|
+
try {
|
|
124
|
+
return realpathSync(input);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function installPrefixFromPath(input: string): string | null {
|
|
131
|
+
const parts = path.resolve(input).split(path.sep);
|
|
132
|
+
for (let i = parts.length - 3; i >= 0; i--) {
|
|
133
|
+
if (
|
|
134
|
+
parts[i] !== "node_modules" ||
|
|
135
|
+
parts[i + 1] !== "@botcord" ||
|
|
136
|
+
parts[i + 2] !== "daemon"
|
|
137
|
+
) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const prefix = parts.slice(0, i).join(path.sep) || path.sep;
|
|
141
|
+
const packageJson = path.join(
|
|
142
|
+
prefix,
|
|
143
|
+
"node_modules",
|
|
144
|
+
"@botcord",
|
|
145
|
+
"daemon",
|
|
146
|
+
"package.json",
|
|
147
|
+
);
|
|
148
|
+
return existsSync(packageJson) ? prefix : null;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
154
|
+
const maybe = timer as { unref?: () => void };
|
|
155
|
+
if (typeof maybe.unref === "function") {
|
|
156
|
+
maybe.unref();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const RESTART_SUPERVISOR_SCRIPT = `
|
|
161
|
+
const cp = require("node:child_process");
|
|
162
|
+
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function alive(pid) {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(pid, 0);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const parentPid = Number(process.env.BOTCORD_RESTART_PARENT_PID || "0");
|
|
178
|
+
const waitMs = Number(process.env.BOTCORD_RESTART_PARENT_EXIT_WAIT_MS || "30000");
|
|
179
|
+
const deadline = Date.now() + waitMs;
|
|
180
|
+
while (parentPid > 0 && alive(parentPid) && Date.now() < deadline) {
|
|
181
|
+
await sleep(250);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const update = process.env.BOTCORD_RESTART_UPDATE === "1";
|
|
185
|
+
const installPrefix = process.env.BOTCORD_RESTART_INSTALL_PREFIX || "";
|
|
186
|
+
if (update && installPrefix) {
|
|
187
|
+
const npmBin = process.env.BOTCORD_RESTART_NPM_BIN || "npm";
|
|
188
|
+
const packageSpec = process.env.BOTCORD_RESTART_PACKAGE || "${DEFAULT_DAEMON_PACKAGE}";
|
|
189
|
+
const timeout = Number(process.env.BOTCORD_RESTART_INSTALL_TIMEOUT_MS || "120000");
|
|
190
|
+
cp.spawnSync(npmBin, ["install", "--prefix", installPrefix, packageSpec], {
|
|
191
|
+
stdio: "ignore",
|
|
192
|
+
env: process.env,
|
|
193
|
+
timeout,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const node = process.env.BOTCORD_RESTART_NODE || process.execPath;
|
|
198
|
+
const entrypoint = process.env.BOTCORD_RESTART_ENTRYPOINT;
|
|
199
|
+
if (!entrypoint) process.exit(1);
|
|
200
|
+
let args = ["start", "--foreground"];
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(process.env.BOTCORD_RESTART_ARGS_JSON || "[]");
|
|
203
|
+
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
204
|
+
args = parsed;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// keep default args
|
|
208
|
+
}
|
|
209
|
+
const child = cp.spawn(node, [entrypoint, ...args], {
|
|
210
|
+
detached: true,
|
|
211
|
+
stdio: "ignore",
|
|
212
|
+
env: { ...process.env, BOTCORD_DAEMON_CHILD: "1" },
|
|
213
|
+
});
|
|
214
|
+
child.unref();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch(() => process.exit(1));
|
|
218
|
+
`;
|