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

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"]),
@@ -81280,7 +81316,8 @@ var SlackConnection = class {
81280
81316
  id: c.id ?? channel,
81281
81317
  name: c.name,
81282
81318
  isIm: c.is_im,
81283
- isPrivate: c.is_private
81319
+ isPrivate: c.is_private,
81320
+ user: c.user
81284
81321
  };
81285
81322
  }
81286
81323
  async listMembers(channel) {
@@ -81450,6 +81487,11 @@ var SlackNameResolver = class {
81450
81487
  try {
81451
81488
  const info = await conn.getChannelInfo(channel);
81452
81489
  if (info.name) this.save(channel, info.name);
81490
+ else if (info.isIm && info.user) {
81491
+ const p = await conn.getUserProfile(info.user);
81492
+ const name = p.realName || p.name;
81493
+ if (name) this.save(channel, `@${name}`);
81494
+ }
81453
81495
  } catch (err) {
81454
81496
  this.nextAttemptAt.set(channel, this.now() + FAIL_TTL_MS);
81455
81497
  this.log?.debug(`slack: name lookup failed for channel ${channel}: ${err.message}`);
@@ -83008,6 +83050,17 @@ var CpClient = class {
83008
83050
  if (this.state !== "READY" && this.state !== "DRAINING") return;
83009
83051
  this.transport?.send(encode(buildEnvelope("integration/channels", snapshot)));
83010
83052
  }
83053
+ /**
83054
+ * Report a CP-owned cron fire (D→C `cron/report` EVT, fire-and-forget) so the
83055
+ * console's `lastRunAt` converges. The local D11 stamp stays authoritative and
83056
+ * the CP upsert is latest-wins, so re-asserting stored stamps on each
83057
+ * (re)connect (see onReady in daemon.ts) makes a dropped report only a delay,
83058
+ * never a loss. No-op unless READY/DRAINING.
83059
+ */
83060
+ emitCronReport(report) {
83061
+ if (this.state !== "READY" && this.state !== "DRAINING") return;
83062
+ this.transport?.send(encode(buildEnvelope("cron/report", report)));
83063
+ }
83011
83064
  request(type, payload) {
83012
83065
  const frame = buildEnvelope(type, payload);
83013
83066
  return this.correlator.request(frame, (e) => this.transport.send(e));
@@ -83094,6 +83147,9 @@ var CpClient = class {
83094
83147
  this.deps.configApply.removeCron(frame.payload.cronId);
83095
83148
  this.reply(frame, "ack", { ok: true });
83096
83149
  return;
83150
+ case "cron/run":
83151
+ this.reply(frame, "ack", this.deps.configApply.runCron(frame.payload.cronId));
83152
+ return;
83097
83153
  case "route/assign": {
83098
83154
  const a = frame.payload;
83099
83155
  this.deps.configApply.applyRouteAssign(a);
@@ -83719,7 +83775,8 @@ function toCronDef(cron) {
83719
83775
  schedule: cron.schedule,
83720
83776
  ...cron.target && cron.target.platform === "slack" ? { target: {
83721
83777
  platform: "slack",
83722
- channel: cron.target.channel
83778
+ channel: cron.target.channel,
83779
+ ...cron.target.integrationId ? { integrationId: cron.target.integrationId } : {}
83723
83780
  } } : {},
83724
83781
  trigger: cron.trigger,
83725
83782
  enabled: cron.enabled !== false,
@@ -84663,7 +84720,7 @@ var Daemon = class Daemon {
84663
84720
  async dispatch(agentId, msg, integrationId) {
84664
84721
  if (this.draining || this.drainingAgents.has(agentId)) {
84665
84722
  this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
84666
- return;
84723
+ return null;
84667
84724
  }
84668
84725
  const conv = new OutputConverger(this.agents.get(agentId).output.mode);
84669
84726
  const rec = new TranscriptRecorder();
@@ -84735,6 +84792,7 @@ var Daemon = class Daemon {
84735
84792
  p.resolveDone();
84736
84793
  }
84737
84794
  this.flushQueued(agentId, sessionId, integrationId);
84795
+ return sessionId;
84738
84796
  }
84739
84797
  /** After `!stop` sends session/cancel, give the agent `cancelBackstopMs` to yield.
84740
84798
  * If the turn is still in flight when the timer fires, the agent ignored the
@@ -85086,6 +85144,7 @@ var Daemon = class Daemon {
85086
85144
  applyIntegrationRemove: (integrationId) => this.cpIntegrations?.remove(integrationId),
85087
85145
  upsertCron: (cron) => this.cpCrons.upsert(cron),
85088
85146
  removeCron: (cronId) => this.cpCrons.remove(cronId),
85147
+ runCron: (cronId) => this.runCronNow(cronId),
85089
85148
  applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
85090
85149
  applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u),
85091
85150
  applyAgentLaunch: async (launch) => {
@@ -85119,9 +85178,15 @@ var Daemon = class Daemon {
85119
85178
  * additionally suppresses all platform output in dispatch.
85120
85179
  */
85121
85180
  async onCronFire(agentId, msg, cron) {
85122
- this.store.setCronLastRun(`${agentId}:${cron.id}`, this.clock.now());
85181
+ const firedAt = this.clock.now();
85182
+ this.store.setCronLastRun(`${agentId}:${cron.id}`, firedAt);
85183
+ if (cron.origin === "cp") this.cpClient?.emitCronReport({
85184
+ cronId: cron.id,
85185
+ agentId,
85186
+ firedAt: new Date(firedAt).toISOString()
85187
+ });
85123
85188
  if (cron.target?.channel) {
85124
- const conn = this.replyConnFor(agentId);
85189
+ const conn = this.replyConnFor(agentId, cron.target.integrationId);
85125
85190
  if (!conn) this.log.warn(`cron "${cron.id}": agent "${agentId}" has no live slack connection — running without output`);
85126
85191
  else try {
85127
85192
  const ts = await conn.postMessage(cron.target.channel, `⏰ ${cron.trigger}`);
@@ -85133,7 +85198,43 @@ var Daemon = class Daemon {
85133
85198
  this.log.warn(`cron "${cron.id}": failed to post trigger to ${cron.target.channel} (${formatErr(err)}) — running without anchor`);
85134
85199
  }
85135
85200
  }
85136
- await this.dispatch(agentId, msg);
85201
+ try {
85202
+ const sessionId = await this.dispatch(agentId, msg, cron.target?.integrationId);
85203
+ if (cron.origin === "cp" && sessionId !== null) this.cpClient?.emitCronReport({
85204
+ cronId: cron.id,
85205
+ agentId,
85206
+ firedAt: new Date(firedAt).toISOString(),
85207
+ status: "success",
85208
+ durationMs: Math.max(0, this.clock.now() - firedAt),
85209
+ sessionId
85210
+ });
85211
+ } catch (err) {
85212
+ if (cron.origin === "cp") this.cpClient?.emitCronReport({
85213
+ cronId: cron.id,
85214
+ agentId,
85215
+ firedAt: new Date(firedAt).toISOString(),
85216
+ status: "failed",
85217
+ durationMs: Math.max(0, this.clock.now() - firedAt),
85218
+ reason: err.message?.slice(0, 300) || "dispatch failed"
85219
+ });
85220
+ throw err;
85221
+ }
85222
+ }
85223
+ /** Console "Run now" (`cron/run` REQ): fire one CP cron immediately. The fire
85224
+ * runs asynchronously — the ack only says whether the cron exists here; the
85225
+ * outcome arrives as normal `cron/report`s. */
85226
+ runCronNow(cronId) {
85227
+ for (const a of this.effectiveAgents()) {
85228
+ const cron = a.crons.find((c) => c.id === cronId && c.origin === "cp");
85229
+ if (!cron) continue;
85230
+ const { msg } = buildSyntheticMessage(a.id, cron, randomUUID());
85231
+ this.onCronFire(a.id, msg, cron).catch((err) => this.log.error(`cron/run dispatch failed for agent "${a.id}": ${formatErr(err)}`));
85232
+ return { ok: true };
85233
+ }
85234
+ return {
85235
+ ok: false,
85236
+ reason: "unknown cron"
85237
+ };
85137
85238
  }
85138
85239
  /** Real CPU utilization (0..1) across all cores since the previous call — the busy
85139
85240
  * fraction of the time delta. Called once per heartbeat, so it reflects load over the
@@ -85180,6 +85281,15 @@ var Daemon = class Daemon {
85180
85281
  integrationId,
85181
85282
  channels
85182
85283
  });
85284
+ for (const a of this.effectiveAgents()) for (const c of a.crons) {
85285
+ if (c.origin !== "cp") continue;
85286
+ const at = this.store.getCronLastRun(`${a.id}:${c.id}`);
85287
+ if (at !== void 0) this.cpClient?.emitCronReport({
85288
+ cronId: c.id,
85289
+ agentId: a.id,
85290
+ firedAt: new Date(at).toISOString()
85291
+ });
85292
+ }
85183
85293
  },
85184
85294
  localState: () => ({
85185
85295
  assignments: [],