@agentconnect.md/daemon 1.0.0-rc.76 → 1.0.0-rc.78

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/index.js CHANGED
@@ -7659,14 +7659,22 @@ const IntegrationSchema = object({
7659
7659
  platform: literal("slack"),
7660
7660
  slack: SlackConfigSchema
7661
7661
  });
7662
+ /** A scheduled trigger for THIS agent: every `schedule` tick, prompt the agent
7663
+ * with `trigger`. `target` is optional output routing — when present the daemon
7664
+ * posts the trigger into that channel and the session replies in its thread;
7665
+ * absent ⇒ headless fire (no platform output). `origin:"cp"` marks CP-pushed
7666
+ * entries (written by cron/upsert, pruned by drop.crons); hand-authored entries
7667
+ * have no origin and are never touched by the CP. */
7662
7668
  const CronDefSchema = object({
7663
7669
  id: string(),
7664
7670
  schedule: string(),
7665
7671
  target: object({
7666
7672
  platform: literal("slack"),
7667
7673
  channel: string()
7668
- }),
7669
- trigger: string()
7674
+ }).optional(),
7675
+ trigger: string(),
7676
+ enabled: boolean().default(true),
7677
+ origin: literal("cp").optional()
7670
7678
  });
7671
7679
  const AgentSchema = object({
7672
7680
  id: string(),
@@ -12912,13 +12920,27 @@ const DrainDone = object({ released: array(SessionKey) });
12912
12920
  /**
12913
12921
  * Cron sinks to the daemon (D5) — protocol §5.4.
12914
12922
  *
12915
- * The CP owns the definition; the daemon owns firing + last-run persistence so
12916
- * crons fire even when the CP is down.
12923
+ * A cron periodically triggers ONE AGENT with a synthetic prompt (`trigger`) to
12924
+ * carry out some work. The CP owns the definition; the daemon owns firing +
12925
+ * last-run persistence, so crons fire even when the CP is down. On receipt the
12926
+ * daemon persists the def into the owning agent's `agent.json` `crons[]` (the
12927
+ * single source of truth, same model as integrations) — surviving a restart
12928
+ * with the CP down.
12929
+ *
12930
+ * `target` is OPTIONAL output routing: when present, the daemon posts the
12931
+ * trigger as a real message in that channel and the agent's session replies in
12932
+ * its thread; when absent the fire is headless (the agent works with no
12933
+ * platform output).
12917
12934
  */
12935
+ const CronTarget = object({
12936
+ platform: _enum(["slack", "telegram"]).default("slack"),
12937
+ channel: string()
12938
+ });
12918
12939
  const CronUpsert = object({
12919
12940
  cronId: string().uuid(),
12941
+ agentId: string().uuid(),
12920
12942
  schedule: string(),
12921
- target: object({ channel: string() }),
12943
+ target: CronTarget.optional(),
12922
12944
  trigger: string(),
12923
12945
  enabled: boolean().default(true)
12924
12946
  });
@@ -23846,6 +23868,11 @@ var LocalStore = class {
23846
23868
  id INTEGER PRIMARY KEY CHECK (id = 1),
23847
23869
  routingEpoch INTEGER, assignments TEXT, globalRules TEXT
23848
23870
  );
23871
+ -- Authoritative last-run per cron (protocol §5.4 — missed-fire compensation).
23872
+ -- key = "<agentId>:<cronId>" (cron defs themselves live in agent.json).
23873
+ CREATE TABLE IF NOT EXISTS cron_runs (
23874
+ key TEXT PRIMARY KEY, lastRunAt INTEGER NOT NULL
23875
+ );
23849
23876
  `);
23850
23877
  this.migrateSessionUsage();
23851
23878
  }
@@ -24046,6 +24073,17 @@ var LocalStore = class {
24046
24073
  globalRules
24047
24074
  });
24048
24075
  }
24076
+ /** Stamp a cron fire (key = `<agentId>:<cronId>`). */
24077
+ setCronLastRun(key, lastRunAt) {
24078
+ this.db.prepare(`INSERT INTO cron_runs (key, lastRunAt) VALUES (@key, @lastRunAt)
24079
+ ON CONFLICT(key) DO UPDATE SET lastRunAt=excluded.lastRunAt`).run({
24080
+ key,
24081
+ lastRunAt
24082
+ });
24083
+ }
24084
+ getCronLastRun(key) {
24085
+ return this.db.prepare("SELECT lastRunAt FROM cron_runs WHERE key = ?").get(key)?.lastRunAt;
24086
+ }
24049
24087
  close() {
24050
24088
  this.db.close();
24051
24089
  }
@@ -82082,7 +82120,7 @@ function buildSyntheticMessage(agentId, cron, traceId) {
82082
82120
  traceId,
82083
82121
  source: "cron",
82084
82122
  platform: "slack",
82085
- channel: cron.target.channel,
82123
+ channel: cron.target?.channel ?? `cron:${cron.id}`,
82086
82124
  thread: `cron:${cron.id}:${traceId}`,
82087
82125
  sender: {
82088
82126
  id: `cron:${cron.id}`,
@@ -82091,25 +82129,52 @@ function buildSyntheticMessage(agentId, cron, traceId) {
82091
82129
  text: cron.trigger,
82092
82130
  mentionedBots: [],
82093
82131
  isDm: false,
82094
- trigger: "cron"
82132
+ trigger: "cron",
82133
+ ...cron.target ? {} : { headless: true }
82095
82134
  }
82096
82135
  };
82097
82136
  }
82137
+ /**
82138
+ * Local scheduler for `agent.json.crons[]` (D5). Jobs are keyed per agent so the
82139
+ * reconciler can converge them on any agent change (design §5.2: crons change →
82140
+ * Scheduler upsert/remove) — `sync` replaces an agent's whole job set, `unregister`
82141
+ * drops it. A malformed schedule skips that one cron (warned), never the rest.
82142
+ */
82098
82143
  var Scheduler = class {
82099
82144
  deps;
82100
- jobs = [];
82145
+ jobs = /* @__PURE__ */ new Map();
82101
82146
  constructor(deps) {
82102
82147
  this.deps = deps;
82103
82148
  }
82104
- register(agentId, cron) {
82105
- this.jobs.push(new E(cron.schedule, () => {
82106
- const { msg } = buildSyntheticMessage(agentId, cron, this.deps.newTraceId());
82107
- this.deps.onFire(agentId, msg);
82108
- }));
82149
+ /** Converge one agent's job set to its current `crons[]` (replace-all, idempotent).
82150
+ * Disabled crons stay on disk but are not scheduled. */
82151
+ sync(agentId, crons) {
82152
+ this.unregister(agentId);
82153
+ const jobs = [];
82154
+ for (const cron of crons) {
82155
+ if (cron.enabled === false) continue;
82156
+ try {
82157
+ jobs.push(new E(cron.schedule, () => {
82158
+ const { msg } = buildSyntheticMessage(agentId, cron, this.deps.newTraceId());
82159
+ this.deps.onFire(agentId, msg, cron);
82160
+ }));
82161
+ } catch (err) {
82162
+ this.deps.warn?.(`scheduler: skipping cron "${cron.id}" of agent "${agentId}": ${err.message}`);
82163
+ }
82164
+ }
82165
+ if (jobs.length) this.jobs.set(agentId, jobs);
82166
+ }
82167
+ /** Stop and drop an agent's jobs (agent removed / deactivated). */
82168
+ unregister(agentId) {
82169
+ for (const j of this.jobs.get(agentId) ?? []) j.stop();
82170
+ this.jobs.delete(agentId);
82171
+ }
82172
+ /** Live job count for one agent (0 when none). */
82173
+ count(agentId) {
82174
+ return this.jobs.get(agentId)?.length ?? 0;
82109
82175
  }
82110
82176
  stop() {
82111
- for (const j of this.jobs) j.stop();
82112
- this.jobs = [];
82177
+ for (const agentId of [...this.jobs.keys()]) this.unregister(agentId);
82113
82178
  }
82114
82179
  };
82115
82180
  //#endregion
@@ -83061,41 +83126,6 @@ async function fetchHeadMtime(root) {
83061
83126
  }
83062
83127
  }
83063
83128
  //#endregion
83064
- //#region src/cp/cp-cron.ts
83065
- /**
83066
- * `CpCronRegistry` — the daemon's set of CP-pushed crons, keyed by `cronId`
83067
- * (distinct from per-agent `agent.crons`). On fire it calls `onFire(cron)`; the
83068
- * daemon resolves `cron.target.channel` to an owning agent and dispatches.
83069
- * `converge` makes the live set exactly a register/ok snapshot.
83070
- */
83071
- var CpCronRegistry = class {
83072
- deps;
83073
- jobs = /* @__PURE__ */ new Map();
83074
- constructor(deps) {
83075
- this.deps = deps;
83076
- }
83077
- upsert(cron) {
83078
- this.remove(cron.cronId);
83079
- if (cron.enabled === false) return;
83080
- this.jobs.set(cron.cronId, new E(cron.schedule, () => this.deps.onFire(cron)));
83081
- }
83082
- remove(cronId) {
83083
- this.jobs.get(cronId)?.stop();
83084
- this.jobs.delete(cronId);
83085
- }
83086
- converge(crons) {
83087
- const keep = new Set(crons.map((c) => c.cronId));
83088
- for (const id of [...this.jobs.keys()]) if (!keep.has(id)) this.remove(id);
83089
- for (const c of crons) this.upsert(c);
83090
- }
83091
- ids() {
83092
- return [...this.jobs.keys()];
83093
- }
83094
- stop() {
83095
- for (const id of [...this.jobs.keys()]) this.remove(id);
83096
- }
83097
- };
83098
- //#endregion
83099
83129
  //#region src/agents/write-agent.ts
83100
83130
  /**
83101
83131
  * Persist CP-owned agent spec onto the daemon's on-disk `agent.json`, which is
@@ -83228,6 +83258,127 @@ function removeAgent(agentsDir, agentId) {
83228
83258
  });
83229
83259
  }
83230
83260
  //#endregion
83261
+ //#region src/agents/write-cron.ts
83262
+ /**
83263
+ * Persist CP-owned crons onto the owning agent's on-disk `agent.json` `crons[]`,
83264
+ * which is the SINGLE SOURCE OF TRUTH (same model as write-integration.ts) — so
83265
+ * CP crons survive a daemon restart with the Control Plane down: the Scheduler
83266
+ * re-registers them from disk alone at start.
83267
+ *
83268
+ * CP entries are marked `origin:"cp"` so they coexist with hand-authored crons:
83269
+ * the CP replaces/removes only its own entries and never touches user ones.
83270
+ *
83271
+ * CRITICAL: like write-agent.ts, we operate on the RAW JSON TEXT (readFileSync →
83272
+ * JSON.parse → mutate only `crons[]` → writeFileSync). We never serialize the
83273
+ * parsed/interpolated `LoadedAgent` — that has `${VAR}` already substituted and
83274
+ * `workspace.path` rewritten absolute. Editing the raw file preserves both.
83275
+ */
83276
+ /** Map the wire def to the agent.json `CronDef` shape (agent-schema). */
83277
+ function toCronDef(cron) {
83278
+ return {
83279
+ id: cron.cronId,
83280
+ schedule: cron.schedule,
83281
+ ...cron.target && cron.target.platform === "slack" ? { target: {
83282
+ platform: "slack",
83283
+ channel: cron.target.channel
83284
+ } } : {},
83285
+ trigger: cron.trigger,
83286
+ enabled: cron.enabled !== false,
83287
+ origin: "cp"
83288
+ };
83289
+ }
83290
+ /**
83291
+ * Upsert one CP cron into the owning agent's `agent.json` `crons[]` (replace the
83292
+ * `origin:"cp"` entry with that `id`, else append). Returns whether the file
83293
+ * CHANGED — an identical re-apply (every register/ok converge re-sends the full
83294
+ * set) skips the write so the file-watcher doesn't churn a reconcile per
83295
+ * reconnect. Returns false (with a warn) when no agent with `cron.agentId`
83296
+ * exists on disk — an orphan cron is not persisted; it self-heals when the
83297
+ * agent spec lands and the CP re-sends.
83298
+ *
83299
+ * The replace slot is matched by `id` AND `origin:"cp"` (mirroring
83300
+ * {@link removeCronDef}): a hand-authored entry that happens to share the id is
83301
+ * NEVER overwritten — the CP touches only its own entries. Such a collision is
83302
+ * skipped with a warn rather than silently converting a user cron into a CP one.
83303
+ */
83304
+ function writeCronDef(agentsDir, cron, deps) {
83305
+ const file = findAgentFileById(agentsDir, cron.agentId);
83306
+ if (!file) {
83307
+ deps.warn?.(`cp: cron ${cron.cronId} references missing agent ${cron.agentId} — not persisted (will retry on next push)`);
83308
+ return false;
83309
+ }
83310
+ const raw = JSON.parse(readFileSync(file, "utf8"));
83311
+ const list = Array.isArray(raw.crons) ? raw.crons : [];
83312
+ const next = toCronDef(cron);
83313
+ const at = (c) => typeof c === "object" && c !== null && c.id === next.id;
83314
+ if (list.find((c) => at(c) && c.origin !== "cp")) {
83315
+ deps.warn?.(`cp: cron ${cron.cronId} collides with a hand-authored cron of the same id on agent ${cron.agentId} — not persisted`);
83316
+ return false;
83317
+ }
83318
+ const idx = list.findIndex((c) => at(c) && c.origin === "cp");
83319
+ if (idx >= 0) {
83320
+ if (JSON.stringify(list[idx]) === JSON.stringify(next)) return false;
83321
+ list[idx] = next;
83322
+ } else list.push(next);
83323
+ raw.crons = list;
83324
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
83325
+ return true;
83326
+ }
83327
+ /**
83328
+ * Remove one CP cron (by id) from whichever agent.json holds it. Scans all agent
83329
+ * files since the owning agent is not part of `cron/remove`. Only `origin:"cp"`
83330
+ * entries are eligible — a hand-authored cron that happens to share the id is
83331
+ * never deleted. Returns whether an entry was removed.
83332
+ */
83333
+ function removeCronDef(agentsDir, cronId) {
83334
+ for (const file of findAgentFiles(agentsDir)) {
83335
+ let raw;
83336
+ try {
83337
+ raw = JSON.parse(readFileSync(file, "utf8"));
83338
+ } catch {
83339
+ continue;
83340
+ }
83341
+ if (!Array.isArray(raw.crons)) continue;
83342
+ const list = raw.crons;
83343
+ const idx = list.findIndex((c) => typeof c === "object" && c !== null && c.id === cronId && c.origin === "cp");
83344
+ if (idx === -1) continue;
83345
+ list.splice(idx, 1);
83346
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
83347
+ return true;
83348
+ }
83349
+ return false;
83350
+ }
83351
+ //#endregion
83352
+ //#region src/cp/cp-cron.ts
83353
+ var CpCronRegistry = class {
83354
+ agentsDir;
83355
+ deps;
83356
+ onChange;
83357
+ constructor(agentsDir, deps, onChange) {
83358
+ this.agentsDir = agentsDir;
83359
+ this.deps = deps;
83360
+ this.onChange = onChange;
83361
+ }
83362
+ /** Add or replace one CP cron on the owning agent's disk file (cron/upsert REQ). */
83363
+ upsert(cron) {
83364
+ if (writeCronDef(this.agentsDir, cron, this.deps)) this.onChange();
83365
+ }
83366
+ /** Splice one CP cron out of whichever agent.json holds it (cron/remove REQ). */
83367
+ remove(cronId) {
83368
+ if (removeCronDef(this.agentsDir, cronId)) this.onChange();
83369
+ }
83370
+ /**
83371
+ * Apply the register/ok snapshot: upsert EACH entry (identical re-applies skip
83372
+ * the write, so a reconnect with an unchanged set is a no-op). Stale ids
83373
+ * arrive separately via `drop.crons[]` → {@link remove}.
83374
+ */
83375
+ converge(crons) {
83376
+ let changed = false;
83377
+ for (const cron of crons ?? []) if (writeCronDef(this.agentsDir, cron, this.deps)) changed = true;
83378
+ if (changed) this.onChange();
83379
+ }
83380
+ };
83381
+ //#endregion
83231
83382
  //#region src/cp/cp-agent-registry.ts
83232
83383
  var CpAgentRegistry = class {
83233
83384
  agentsDir;
@@ -83575,6 +83726,7 @@ var Daemon = class Daemon {
83575
83726
  warn: (m) => this.log.warn(m)
83576
83727
  }, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
83577
83728
  this.cpIntegrations = new CpIntegrationRegistry(this.agentsDir, { warn: (m) => this.log.warn(m) }, () => void this.reconcile().catch((err) => this.log.error(`cp: integration reconcile failed: ${err.stack ?? err}`)));
83729
+ this.cpCrons = new CpCronRegistry(this.agentsDir, { warn: (m) => this.log.warn(m) }, () => void this.reconcile().catch((err) => this.log.error(`cp: cron reconcile failed: ${err.stack ?? err}`)));
83578
83730
  for (const a of this.effectiveAgents()) {
83579
83731
  this.agents.set(a.id, a);
83580
83732
  this.prefetchClone(a);
@@ -83622,8 +83774,9 @@ var Daemon = class Daemon {
83622
83774
  fetchThreadHistory: (agentId, channel, threadTs) => this.fetchThreadHistory(agentId, channel, threadTs)
83623
83775
  });
83624
83776
  this.scheduler = new Scheduler({
83625
- onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
83626
- newTraceId: () => randomUUID()
83777
+ onFire: (agentId, msg, cron) => void this.onCronFire(agentId, msg, cron).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
83778
+ newTraceId: () => randomUUID(),
83779
+ warn: (m) => this.log.warn(m)
83627
83780
  });
83628
83781
  const groups = consolidate(agents);
83629
83782
  this.botUserIds = {};
@@ -83651,11 +83804,8 @@ var Daemon = class Daemon {
83651
83804
  this.connections.push(conn);
83652
83805
  this.refreshChannels(conn);
83653
83806
  }
83654
- let cronCount = 0;
83655
- for (const a of agents) for (const c of a.crons) {
83656
- this.scheduler.register(a.id, c);
83657
- cronCount++;
83658
- }
83807
+ for (const a of agents) this.scheduler.sync(a.id, a.crons);
83808
+ const cronCount = agents.reduce((n, a) => n + this.scheduler.count(a.id), 0);
83659
83809
  if (cronCount) this.log.info(`registered ${cronCount} cron(s)`);
83660
83810
  this.watcher = watch$1(this.agentsDir, {
83661
83811
  ignoreInitial: true,
@@ -83740,10 +83890,12 @@ var Daemon = class Daemon {
83740
83890
  }
83741
83891
  this.hostStarts.delete(id);
83742
83892
  this.agents.delete(id);
83893
+ this.scheduler.unregister(id);
83743
83894
  }
83744
83895
  for (const change of toChange) {
83745
83896
  const a = change.agent;
83746
83897
  this.agents.set(a.id, a);
83898
+ this.scheduler.sync(a.id, a.crons);
83747
83899
  if (change.hostRespawn || change.workspace) {
83748
83900
  const host = this.hosts.get(a.id);
83749
83901
  if (host) {
@@ -83758,6 +83910,7 @@ var Daemon = class Daemon {
83758
83910
  for (const a of toStart) {
83759
83911
  this.agents.set(a.id, a);
83760
83912
  this.prefetchClone(a);
83913
+ this.scheduler.sync(a.id, a.crons);
83761
83914
  }
83762
83915
  if (toStart.some((a) => a.integrations.some((i) => i.platform === "slack")) || toStop.length) await this.reconcileSlackConnections();
83763
83916
  }
@@ -84069,7 +84222,7 @@ var Daemon = class Daemon {
84069
84222
  }
84070
84223
  const conv = new OutputConverger(this.agents.get(agentId).output.mode);
84071
84224
  const rec = new TranscriptRecorder();
84072
- const replyConn = this.replyConnFor(agentId, integrationId);
84225
+ const replyConn = msg.headless ? void 0 : this.replyConnFor(agentId, integrationId);
84073
84226
  const wasRunning = this.hostStarts.has(agentId);
84074
84227
  const statusThread = msg.thread ?? msg.msgId;
84075
84228
  const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
@@ -84270,6 +84423,13 @@ var Daemon = class Daemon {
84270
84423
  const intId = integrationId ?? this.agents.get(agentId)?.integrations[0]?.id;
84271
84424
  return intId ? this.connByIntegration.get(intId) : void 0;
84272
84425
  }
84426
+ /** The CP-owned cron ids currently on disk (register `localState.crons` — the
84427
+ * CP computes `drop.crons` against this; hand-authored crons are not reported). */
84428
+ cpCronIds() {
84429
+ const out = [];
84430
+ for (const a of this.effectiveAgents()) for (const c of a.crons) if (c.origin === "cp") out.push(c.id);
84431
+ return out;
84432
+ }
84273
84433
  /** Stop and evict an ACP adapter child, returning the agent to `provisioned`
84274
84434
  * (config kept; the next message lazily re-spawns it). Idempotent. The teardown
84275
84435
  * is registered in `hostStopping` so a concurrent ensureHostAsync waits for it
@@ -84444,9 +84604,10 @@ var Daemon = class Daemon {
84444
84604
  if (ignored.length) this.log.warn(`cp: ignored config keys: ${ignored.join(", ")}`);
84445
84605
  },
84446
84606
  applyReconcileSnapshot: (snap) => {
84447
- this.cpCrons?.converge(snap.crons);
84448
84607
  this.cpAgents?.converge(snap.agents);
84449
84608
  this.cpIntegrations?.converge(snap.integrations);
84609
+ for (const id of snap.drop.crons) this.cpCrons?.remove(id);
84610
+ this.cpCrons?.converge(snap.crons);
84450
84611
  this.cpRouting?.converge({
84451
84612
  routingEpoch: snap.routingEpoch,
84452
84613
  assignments: snap.assignments,
@@ -84486,26 +84647,31 @@ var Daemon = class Daemon {
84486
84647
  applyDaemonUpgrade: (req) => this.scheduleFleetExit("upgrade", req.targetVersion)
84487
84648
  };
84488
84649
  }
84489
- /** A CP cron fired: build a synthetic message and run it through the normal routing path. */
84490
- onCpCronFire(cron) {
84491
- const traceId = randomUUID();
84492
- const msg = {
84493
- msgId: `cpcron:${cron.cronId}:${traceId}`,
84494
- traceId,
84495
- source: "cron",
84496
- platform: "slack",
84497
- channel: cron.target.channel,
84498
- thread: `cpcron:${cron.cronId}:${traceId}`,
84499
- sender: {
84500
- id: `cpcron:${cron.cronId}`,
84501
- isBot: false
84502
- },
84503
- text: cron.trigger,
84504
- mentionedBots: [],
84505
- isDm: false,
84506
- trigger: "cron"
84507
- };
84508
- this.onInbound(msg);
84650
+ /**
84651
+ * A cron fired for `agentId` (§8.6 bypass — the cron names its agent; no
84652
+ * routing). The fire is stamped into D11 first (the daemon owns last-run,
84653
+ * protocol §5.4). With a target channel, the trigger text is posted there as a
84654
+ * real ANCHOR message and the session threads its replies under it —
84655
+ * equivalent to a user posting the trigger in-channel. Without one (or when
84656
+ * the anchor can't be posted) the turn still runs; a `headless` fire
84657
+ * additionally suppresses all platform output in dispatch.
84658
+ */
84659
+ async onCronFire(agentId, msg, cron) {
84660
+ this.store.setCronLastRun(`${agentId}:${cron.id}`, this.clock.now());
84661
+ if (cron.target?.channel) {
84662
+ const conn = this.replyConnFor(agentId);
84663
+ if (!conn) this.log.warn(`cron "${cron.id}": agent "${agentId}" has no live slack connection — running without output`);
84664
+ else try {
84665
+ const ts = await conn.postMessage(cron.target.channel, `⏰ ${cron.trigger}`);
84666
+ if (ts) msg = {
84667
+ ...msg,
84668
+ thread: ts
84669
+ };
84670
+ } catch (err) {
84671
+ this.log.warn(`cron "${cron.id}": failed to post trigger to ${cron.target.channel} (${formatErr(err)}) — running without anchor`);
84672
+ }
84673
+ }
84674
+ await this.dispatch(agentId, msg);
84509
84675
  }
84510
84676
  /** Real CPU utilization (0..1) across all cores since the previous call — the busy
84511
84677
  * fraction of the time delta. Called once per heartbeat, so it reflects load over the
@@ -84525,7 +84691,6 @@ var Daemon = class Daemon {
84525
84691
  return;
84526
84692
  }
84527
84693
  const echoDaemonId = this.opts.overrides?.daemonId;
84528
- this.cpCrons = new CpCronRegistry({ onFire: (cron) => this.onCpCronFire(cron) });
84529
84694
  const url = cp.url;
84530
84695
  this.cpClient = new CpClient({
84531
84696
  url,
@@ -84556,7 +84721,7 @@ var Daemon = class Daemon {
84556
84721
  },
84557
84722
  localState: () => ({
84558
84723
  assignments: [],
84559
- crons: this.cpCrons?.ids() ?? [],
84724
+ crons: this.cpCronIds(),
84560
84725
  leases: []
84561
84726
  }),
84562
84727
  loadSnapshot: () => ({
@@ -84640,7 +84805,6 @@ var Daemon = class Daemon {
84640
84805
  const errors = [];
84641
84806
  this.scheduler?.stop();
84642
84807
  await Promise.resolve(this.cpClient?.stop()).catch((e) => errors.push(e));
84643
- this.cpCrons?.stop();
84644
84808
  for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
84645
84809
  for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
84646
84810
  await Promise.resolve(this.mcp?.stop()).catch((e) => errors.push(e));