@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/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) {