@agentconnect.md/daemon 1.0.0-rc.101 → 1.0.0-rc.102

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
@@ -7670,7 +7670,8 @@ const CronDefSchema = object({
7670
7670
  schedule: string(),
7671
7671
  target: object({
7672
7672
  platform: literal("slack"),
7673
- channel: string()
7673
+ channel: string(),
7674
+ integrationId: string().optional()
7674
7675
  }).optional(),
7675
7676
  trigger: string(),
7676
7677
  enabled: boolean().default(true),
@@ -12935,7 +12936,8 @@ const DrainDone = object({ released: array(SessionKey) });
12935
12936
  */
12936
12937
  const CronTarget = object({
12937
12938
  platform: _enum(["slack", "telegram"]).default("slack"),
12938
- channel: string()
12939
+ channel: string(),
12940
+ integrationId: string().uuid().optional()
12939
12941
  });
12940
12942
  const CronUpsert = object({
12941
12943
  cronId: string().uuid(),
@@ -12946,6 +12948,36 @@ const CronUpsert = object({
12946
12948
  enabled: boolean().default(true)
12947
12949
  });
12948
12950
  const CronRemove = object({ cronId: string().uuid() });
12951
+ /**
12952
+ * `cron/report` (D→C EVT, fire-and-forget) — one CP-owned cron fired. The
12953
+ * daemon stamps the fire into its local store first (it stays authoritative,
12954
+ * §5.4) and reports it here so the console's `lastRunAt` converges; the CP
12955
+ * upsert is latest-wins, so the daemon re-asserting its stored stamps on
12956
+ * reconnect (fires while the CP was unreachable) is idempotent and can never
12957
+ * regress the value. Hand-authored (no-origin) crons are never reported.
12958
+ *
12959
+ * Two reports per fire, keyed by `(cronId, firedAt)`: the FIRE report (no
12960
+ * `status`) opens the run, and the COMPLETION report (with `status` + outcome
12961
+ * fields) closes it once the dispatched turn ends — feeding the console's
12962
+ * per-schedule run history. A completion without a prior fire report (CP was
12963
+ * down at fire time) still creates the run row.
12964
+ */
12965
+ const CronRunStatus = _enum(["success", "failed"]);
12966
+ const CronReport = object({
12967
+ cronId: string().uuid(),
12968
+ agentId: string().uuid(),
12969
+ firedAt: string().datetime(),
12970
+ status: CronRunStatus.optional(),
12971
+ durationMs: number().int().nonnegative().optional(),
12972
+ sessionId: string().optional(),
12973
+ reason: string().optional()
12974
+ });
12975
+ /**
12976
+ * `cron/run` (C→D REQ → ack) — fire one CP-owned cron NOW (console "Run now").
12977
+ * The daemon accepts (`ok:true`) and runs the fire asynchronously — outcome
12978
+ * arrives as normal `cron/report`s; `ok:false` when it holds no such cron.
12979
+ */
12980
+ const CronRunNow = object({ cronId: string().uuid() });
12949
12981
  //#endregion
12950
12982
  //#region ../protocol/dist/frames/secrets.js
12951
12983
  /**
@@ -13546,6 +13578,8 @@ const FRAME_SCHEMAS = {
13546
13578
  "drain/done": DrainDone,
13547
13579
  "cron/upsert": CronUpsert,
13548
13580
  "cron/remove": CronRemove,
13581
+ "cron/report": CronReport,
13582
+ "cron/run": CronRunNow,
13549
13583
  "integration/upsert": IntegrationUpsert,
13550
13584
  "integration/remove": IntegrationRemove,
13551
13585
  "integration/channels": IntegrationChannels,
@@ -13651,6 +13685,8 @@ discriminatedUnion("type", [
13651
13685
  frame("drain/done", FRAME_SCHEMAS["drain/done"]),
13652
13686
  frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
13653
13687
  frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
13688
+ frame("cron/report", FRAME_SCHEMAS["cron/report"]),
13689
+ frame("cron/run", FRAME_SCHEMAS["cron/run"]),
13654
13690
  frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
13655
13691
  frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
13656
13692
  frame("integration/channels", FRAME_SCHEMAS["integration/channels"]),
@@ -83008,6 +83044,17 @@ var CpClient = class {
83008
83044
  if (this.state !== "READY" && this.state !== "DRAINING") return;
83009
83045
  this.transport?.send(encode(buildEnvelope("integration/channels", snapshot)));
83010
83046
  }
83047
+ /**
83048
+ * Report a CP-owned cron fire (D→C `cron/report` EVT, fire-and-forget) so the
83049
+ * console's `lastRunAt` converges. The local D11 stamp stays authoritative and
83050
+ * the CP upsert is latest-wins, so re-asserting stored stamps on each
83051
+ * (re)connect (see onReady in daemon.ts) makes a dropped report only a delay,
83052
+ * never a loss. No-op unless READY/DRAINING.
83053
+ */
83054
+ emitCronReport(report) {
83055
+ if (this.state !== "READY" && this.state !== "DRAINING") return;
83056
+ this.transport?.send(encode(buildEnvelope("cron/report", report)));
83057
+ }
83011
83058
  request(type, payload) {
83012
83059
  const frame = buildEnvelope(type, payload);
83013
83060
  return this.correlator.request(frame, (e) => this.transport.send(e));
@@ -83094,6 +83141,9 @@ var CpClient = class {
83094
83141
  this.deps.configApply.removeCron(frame.payload.cronId);
83095
83142
  this.reply(frame, "ack", { ok: true });
83096
83143
  return;
83144
+ case "cron/run":
83145
+ this.reply(frame, "ack", this.deps.configApply.runCron(frame.payload.cronId));
83146
+ return;
83097
83147
  case "route/assign": {
83098
83148
  const a = frame.payload;
83099
83149
  this.deps.configApply.applyRouteAssign(a);
@@ -83719,7 +83769,8 @@ function toCronDef(cron) {
83719
83769
  schedule: cron.schedule,
83720
83770
  ...cron.target && cron.target.platform === "slack" ? { target: {
83721
83771
  platform: "slack",
83722
- channel: cron.target.channel
83772
+ channel: cron.target.channel,
83773
+ ...cron.target.integrationId ? { integrationId: cron.target.integrationId } : {}
83723
83774
  } } : {},
83724
83775
  trigger: cron.trigger,
83725
83776
  enabled: cron.enabled !== false,
@@ -84663,7 +84714,7 @@ var Daemon = class Daemon {
84663
84714
  async dispatch(agentId, msg, integrationId) {
84664
84715
  if (this.draining || this.drainingAgents.has(agentId)) {
84665
84716
  this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
84666
- return;
84717
+ return null;
84667
84718
  }
84668
84719
  const conv = new OutputConverger(this.agents.get(agentId).output.mode);
84669
84720
  const rec = new TranscriptRecorder();
@@ -84735,6 +84786,7 @@ var Daemon = class Daemon {
84735
84786
  p.resolveDone();
84736
84787
  }
84737
84788
  this.flushQueued(agentId, sessionId, integrationId);
84789
+ return sessionId;
84738
84790
  }
84739
84791
  /** After `!stop` sends session/cancel, give the agent `cancelBackstopMs` to yield.
84740
84792
  * If the turn is still in flight when the timer fires, the agent ignored the
@@ -85086,6 +85138,7 @@ var Daemon = class Daemon {
85086
85138
  applyIntegrationRemove: (integrationId) => this.cpIntegrations?.remove(integrationId),
85087
85139
  upsertCron: (cron) => this.cpCrons.upsert(cron),
85088
85140
  removeCron: (cronId) => this.cpCrons.remove(cronId),
85141
+ runCron: (cronId) => this.runCronNow(cronId),
85089
85142
  applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
85090
85143
  applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u),
85091
85144
  applyAgentLaunch: async (launch) => {
@@ -85119,9 +85172,15 @@ var Daemon = class Daemon {
85119
85172
  * additionally suppresses all platform output in dispatch.
85120
85173
  */
85121
85174
  async onCronFire(agentId, msg, cron) {
85122
- this.store.setCronLastRun(`${agentId}:${cron.id}`, this.clock.now());
85175
+ const firedAt = this.clock.now();
85176
+ this.store.setCronLastRun(`${agentId}:${cron.id}`, firedAt);
85177
+ if (cron.origin === "cp") this.cpClient?.emitCronReport({
85178
+ cronId: cron.id,
85179
+ agentId,
85180
+ firedAt: new Date(firedAt).toISOString()
85181
+ });
85123
85182
  if (cron.target?.channel) {
85124
- const conn = this.replyConnFor(agentId);
85183
+ const conn = this.replyConnFor(agentId, cron.target.integrationId);
85125
85184
  if (!conn) this.log.warn(`cron "${cron.id}": agent "${agentId}" has no live slack connection — running without output`);
85126
85185
  else try {
85127
85186
  const ts = await conn.postMessage(cron.target.channel, `⏰ ${cron.trigger}`);
@@ -85133,7 +85192,43 @@ var Daemon = class Daemon {
85133
85192
  this.log.warn(`cron "${cron.id}": failed to post trigger to ${cron.target.channel} (${formatErr(err)}) — running without anchor`);
85134
85193
  }
85135
85194
  }
85136
- await this.dispatch(agentId, msg);
85195
+ try {
85196
+ const sessionId = await this.dispatch(agentId, msg, cron.target?.integrationId);
85197
+ if (cron.origin === "cp" && sessionId !== null) this.cpClient?.emitCronReport({
85198
+ cronId: cron.id,
85199
+ agentId,
85200
+ firedAt: new Date(firedAt).toISOString(),
85201
+ status: "success",
85202
+ durationMs: Math.max(0, this.clock.now() - firedAt),
85203
+ sessionId
85204
+ });
85205
+ } catch (err) {
85206
+ if (cron.origin === "cp") this.cpClient?.emitCronReport({
85207
+ cronId: cron.id,
85208
+ agentId,
85209
+ firedAt: new Date(firedAt).toISOString(),
85210
+ status: "failed",
85211
+ durationMs: Math.max(0, this.clock.now() - firedAt),
85212
+ reason: err.message?.slice(0, 300) || "dispatch failed"
85213
+ });
85214
+ throw err;
85215
+ }
85216
+ }
85217
+ /** Console "Run now" (`cron/run` REQ): fire one CP cron immediately. The fire
85218
+ * runs asynchronously — the ack only says whether the cron exists here; the
85219
+ * outcome arrives as normal `cron/report`s. */
85220
+ runCronNow(cronId) {
85221
+ for (const a of this.effectiveAgents()) {
85222
+ const cron = a.crons.find((c) => c.id === cronId && c.origin === "cp");
85223
+ if (!cron) continue;
85224
+ const { msg } = buildSyntheticMessage(a.id, cron, randomUUID());
85225
+ this.onCronFire(a.id, msg, cron).catch((err) => this.log.error(`cron/run dispatch failed for agent "${a.id}": ${formatErr(err)}`));
85226
+ return { ok: true };
85227
+ }
85228
+ return {
85229
+ ok: false,
85230
+ reason: "unknown cron"
85231
+ };
85137
85232
  }
85138
85233
  /** Real CPU utilization (0..1) across all cores since the previous call — the busy
85139
85234
  * fraction of the time delta. Called once per heartbeat, so it reflects load over the
@@ -85180,6 +85275,15 @@ var Daemon = class Daemon {
85180
85275
  integrationId,
85181
85276
  channels
85182
85277
  });
85278
+ for (const a of this.effectiveAgents()) for (const c of a.crons) {
85279
+ if (c.origin !== "cp") continue;
85280
+ const at = this.store.getCronLastRun(`${a.id}:${c.id}`);
85281
+ if (at !== void 0) this.cpClient?.emitCronReport({
85282
+ cronId: c.id,
85283
+ agentId: a.id,
85284
+ firedAt: new Date(at).toISOString()
85285
+ });
85286
+ }
85183
85287
  },
85184
85288
  localState: () => ({
85185
85289
  assignments: [],