@botcord/daemon 0.2.20 → 0.2.22
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/agent-workspace.d.ts +10 -4
- package/dist/agent-workspace.js +57 -19
- package/dist/daemon.js +28 -1
- package/dist/gateway/channels/botcord.js +25 -10
- package/dist/gateway/dispatcher.js +91 -12
- package/dist/index.js +11 -0
- package/dist/provision.d.ts +32 -0
- package/dist/provision.js +52 -1
- package/package.json +1 -1
- package/src/__tests__/agent-workspace.test.ts +45 -0
- package/src/__tests__/provision.test.ts +80 -0
- package/src/agent-workspace.ts +60 -19
- package/src/daemon.ts +31 -1
- package/src/gateway/channels/botcord.ts +41 -16
- package/src/gateway/dispatcher.ts +94 -11
- package/src/index.ts +15 -0
- package/src/provision.ts +86 -1
package/src/index.ts
CHANGED
|
@@ -851,6 +851,21 @@ function cmdTranscriptTail(args: ParsedArgs): Promise<void> | void {
|
|
|
851
851
|
const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
|
|
852
852
|
if (!existsSync(file)) {
|
|
853
853
|
console.error(`no transcript at ${file}`);
|
|
854
|
+
let cfg: DaemonConfig | null = null;
|
|
855
|
+
try {
|
|
856
|
+
cfg = loadConfig();
|
|
857
|
+
} catch {
|
|
858
|
+
// ignore — config may simply not exist yet
|
|
859
|
+
}
|
|
860
|
+
const enabled = resolveTranscriptEnabled(
|
|
861
|
+
process.env.BOTCORD_TRANSCRIPT,
|
|
862
|
+
cfg?.transcript?.enabled === true,
|
|
863
|
+
);
|
|
864
|
+
if (!enabled) {
|
|
865
|
+
console.error(
|
|
866
|
+
"hint: transcripts are disabled (default-off). Run `botcord-daemon transcript enable` and restart the daemon, then send a new message.",
|
|
867
|
+
);
|
|
868
|
+
}
|
|
854
869
|
process.exit(1);
|
|
855
870
|
}
|
|
856
871
|
const follow = args.flags.f === true || args.flags.follow === true;
|
package/src/provision.ts
CHANGED
|
@@ -57,6 +57,30 @@ import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
|
|
|
57
57
|
import { log as daemonLog } from "./log.js";
|
|
58
58
|
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Information passed to {@link OnAgentInstalledHook} after a successful
|
|
62
|
+
* provision. Mirrors the credential fields the daemon's per-agent caches
|
|
63
|
+
* (`credentialPathByAgentId`, `hubUrlByAgentId`, `displayNameByAgent`) read at
|
|
64
|
+
* boot — fired again here so hot-provisioned agents land in those caches
|
|
65
|
+
* without waiting for a daemon restart.
|
|
66
|
+
*/
|
|
67
|
+
export interface InstalledAgentInfo {
|
|
68
|
+
agentId: string;
|
|
69
|
+
credentialsFile: string;
|
|
70
|
+
hubUrl: string;
|
|
71
|
+
displayName?: string;
|
|
72
|
+
runtime?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hook fired after `installLocalAgent` / `installExistingOpenclawBinding`
|
|
77
|
+
* finish successfully. Synchronous on purpose — the caller updates a few
|
|
78
|
+
* in-memory maps and we don't want a slow hook to delay the control-frame
|
|
79
|
+
* ack. Throwing from the hook is treated as a programmer error and
|
|
80
|
+
* surfaced; rollback of the install is the caller's responsibility.
|
|
81
|
+
*/
|
|
82
|
+
export type OnAgentInstalledHook = (info: InstalledAgentInfo) => void;
|
|
83
|
+
|
|
60
84
|
/** Options accepted by {@link createProvisioner}. */
|
|
61
85
|
export interface ProvisionerOptions {
|
|
62
86
|
/** Live gateway handle used to hot-plug channels. */
|
|
@@ -74,6 +98,15 @@ export interface ProvisionerOptions {
|
|
|
74
98
|
* extra round-trip.
|
|
75
99
|
*/
|
|
76
100
|
policyResolver?: PolicyResolverLike;
|
|
101
|
+
/**
|
|
102
|
+
* Optional hook called after each successful agent install (whether via
|
|
103
|
+
* `provision_agent` or `installExistingOpenclawBinding`). The daemon
|
|
104
|
+
* wires this to write the new agent into its boot-seeded caches —
|
|
105
|
+
* without it, `room-context-fetcher` keeps emitting
|
|
106
|
+
* `daemon.room-context.no-credentials` for hot-provisioned agents until
|
|
107
|
+
* the next restart.
|
|
108
|
+
*/
|
|
109
|
+
onAgentInstalled?: OnAgentInstalledHook;
|
|
77
110
|
}
|
|
78
111
|
|
|
79
112
|
/** The value a frame handler returns (minus the `id` which the channel fills in). */
|
|
@@ -90,6 +123,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
90
123
|
const gateway = opts.gateway;
|
|
91
124
|
const register = opts.register ?? BotCordClient.register;
|
|
92
125
|
const policyResolver = opts.policyResolver;
|
|
126
|
+
const onAgentInstalled = opts.onAgentInstalled;
|
|
93
127
|
|
|
94
128
|
return async (frame: ControlFrame): Promise<AckBody> => {
|
|
95
129
|
daemonLog.debug("provision.dispatch", { type: frame.type, id: frame.id });
|
|
@@ -137,7 +171,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
137
171
|
runtime: pickRuntime(params) ?? null,
|
|
138
172
|
name: params.name ?? null,
|
|
139
173
|
});
|
|
140
|
-
const agent = await provisionAgent(params, { gateway, register });
|
|
174
|
+
const agent = await provisionAgent(params, { gateway, register, onAgentInstalled });
|
|
141
175
|
// Seed the policy resolver from the optional `defaultAttention` /
|
|
142
176
|
// `attentionKeywords` fields (PR3, control-frame.ts). Hub builds that
|
|
143
177
|
// don't yet emit these stay backwards-compatible — the resolver just
|
|
@@ -266,6 +300,7 @@ interface ProvisionedAgent {
|
|
|
266
300
|
interface ProvisionCtx {
|
|
267
301
|
gateway: Gateway;
|
|
268
302
|
register: typeof BotCordClient.register;
|
|
303
|
+
onAgentInstalled?: OnAgentInstalledHook;
|
|
269
304
|
}
|
|
270
305
|
|
|
271
306
|
const openclawProvisionLocks = new Map<string, Promise<unknown>>();
|
|
@@ -428,6 +463,32 @@ async function installLocalAgent(
|
|
|
428
463
|
throw err;
|
|
429
464
|
}
|
|
430
465
|
|
|
466
|
+
// Update the daemon's boot-seeded per-agent caches in place. Without this
|
|
467
|
+
// a hot-provisioned agent keeps missing `credentialPathByAgentId` /
|
|
468
|
+
// `hubUrlByAgentId` / `displayNameByAgent` until the next daemon restart,
|
|
469
|
+
// and `room-context-fetcher` logs `daemon.room-context.no-credentials` on
|
|
470
|
+
// every turn for it (so the system-context loses the room block — member
|
|
471
|
+
// names, rule, role).
|
|
472
|
+
if (ctx.onAgentInstalled) {
|
|
473
|
+
try {
|
|
474
|
+
ctx.onAgentInstalled({
|
|
475
|
+
agentId: credentials.agentId,
|
|
476
|
+
credentialsFile,
|
|
477
|
+
hubUrl: credentials.hubUrl,
|
|
478
|
+
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
479
|
+
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
480
|
+
});
|
|
481
|
+
} catch (err) {
|
|
482
|
+
// Hook misbehavior must not fail the install — the agent is already
|
|
483
|
+
// on disk + in the gateway. Surface it loudly so the daemon owner
|
|
484
|
+
// notices the cache is out of sync.
|
|
485
|
+
daemonLog.error("provision.onAgentInstalled threw — caches may be stale", {
|
|
486
|
+
agentId: credentials.agentId,
|
|
487
|
+
error: err instanceof Error ? err.message : String(err),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
431
492
|
daemonLog.info("agent provisioned", {
|
|
432
493
|
agentId: credentials.agentId,
|
|
433
494
|
credentialsFile,
|
|
@@ -490,6 +551,26 @@ async function installExistingOpenclawBinding(
|
|
|
490
551
|
});
|
|
491
552
|
}
|
|
492
553
|
upsertManagedRouteForCredentials(credentials, cfg, ctx.gateway);
|
|
554
|
+
// Same cache-warmup as `installLocalAgent` — re-binding an existing
|
|
555
|
+
// openclaw agent at runtime should also land it in the daemon's
|
|
556
|
+
// per-agent maps, otherwise room-context lookups stay broken until
|
|
557
|
+
// restart.
|
|
558
|
+
if (ctx.onAgentInstalled) {
|
|
559
|
+
try {
|
|
560
|
+
ctx.onAgentInstalled({
|
|
561
|
+
agentId: credentials.agentId,
|
|
562
|
+
credentialsFile,
|
|
563
|
+
hubUrl: credentials.hubUrl,
|
|
564
|
+
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
565
|
+
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
566
|
+
});
|
|
567
|
+
} catch (err) {
|
|
568
|
+
daemonLog.error("provision.onAgentInstalled threw — caches may be stale", {
|
|
569
|
+
agentId: credentials.agentId,
|
|
570
|
+
error: err instanceof Error ? err.message : String(err),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
493
574
|
return {
|
|
494
575
|
agentId: credentials.agentId,
|
|
495
576
|
hubUrl: credentials.hubUrl,
|
|
@@ -658,9 +739,11 @@ export async function adoptDiscoveredOpenclawAgents(ctx: {
|
|
|
658
739
|
cfg?: DaemonConfig;
|
|
659
740
|
timeoutMs?: number;
|
|
660
741
|
probe?: WsEndpointProbeFn;
|
|
742
|
+
onAgentInstalled?: OnAgentInstalledHook;
|
|
661
743
|
}): Promise<AdoptDiscoveredOpenclawAgentsResult> {
|
|
662
744
|
const register = ctx.register ?? BotCordClient.register;
|
|
663
745
|
const cfg = ctx.cfg ?? loadConfig();
|
|
746
|
+
const onAgentInstalled = ctx.onAgentInstalled;
|
|
664
747
|
const result: AdoptDiscoveredOpenclawAgentsResult = {
|
|
665
748
|
adopted: [],
|
|
666
749
|
skipped: [],
|
|
@@ -733,12 +816,14 @@ export async function adoptDiscoveredOpenclawAgents(ctx: {
|
|
|
733
816
|
const credentials = await materializeCredentials(params, freshCfg, {
|
|
734
817
|
gateway: ctx.gateway,
|
|
735
818
|
register,
|
|
819
|
+
...(onAgentInstalled ? { onAgentInstalled } : {}),
|
|
736
820
|
}, undefined);
|
|
737
821
|
const installed = await installLocalAgent(credentials, {
|
|
738
822
|
gateway: ctx.gateway,
|
|
739
823
|
register,
|
|
740
824
|
cfg: freshCfg,
|
|
741
825
|
source: "adopted-openclaw",
|
|
826
|
+
...(onAgentInstalled ? { onAgentInstalled } : {}),
|
|
742
827
|
});
|
|
743
828
|
result.adopted.push(installed.agentId);
|
|
744
829
|
} catch (err) {
|