@cfio/cohort-sync 0.29.0 → 0.31.0

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
@@ -11655,8 +11655,8 @@ function humanizeMs(ms) {
11655
11655
  if (ms >= 6e4) return `${Math.round(ms / 6e4)}m`;
11656
11656
  return `${Math.round(ms / 1e3)}s`;
11657
11657
  }
11658
- function mapCronJob(job, resolveAgentName) {
11659
- return {
11658
+ function mapCronJob(job, resolveAgentName, tracker) {
11659
+ const base = {
11660
11660
  id: job.id,
11661
11661
  text: job.name,
11662
11662
  schedule: formatSchedule(job.schedule),
@@ -11665,9 +11665,58 @@ function mapCronJob(job, resolveAgentName) {
11665
11665
  nextRun: job.state?.nextRunAtMs,
11666
11666
  lastRun: job.state?.lastRunAtMs,
11667
11667
  lastStatus: job.state?.lastRunStatus,
11668
- lastError: job.state?.lastError ?? null
11668
+ lastError: job.state?.lastError ?? null,
11669
+ message: job.payload?.message
11669
11670
  };
11671
+ if (tracker) {
11672
+ const stamps = tracker.observe(job.id, base);
11673
+ base.createdAt = stamps.createdAt;
11674
+ base.updatedAt = stamps.updatedAt;
11675
+ }
11676
+ return base;
11670
11677
  }
11678
+ function fingerprintJob(job) {
11679
+ return [
11680
+ job.text,
11681
+ job.schedule,
11682
+ job.agentId,
11683
+ job.enabled ? "1" : "0",
11684
+ job.message ?? ""
11685
+ ].join("");
11686
+ }
11687
+ var CronTimestampTracker = class {
11688
+ records = /* @__PURE__ */ new Map();
11689
+ /**
11690
+ * Observe a job. Returns the current `{ createdAt, updatedAt }` for the job.
11691
+ * Mutates internal state. Idempotent for unchanged jobs (returns the prior
11692
+ * `updatedAt` when the fingerprint hasn't changed).
11693
+ *
11694
+ * `now` is injectable for tests.
11695
+ */
11696
+ observe(jobId, job, now = Date.now()) {
11697
+ const fingerprint = fingerprintJob(job);
11698
+ const existing = this.records.get(jobId);
11699
+ if (!existing) {
11700
+ const record = { createdAt: now, updatedAt: now, fingerprint };
11701
+ this.records.set(jobId, record);
11702
+ return { createdAt: record.createdAt, updatedAt: record.updatedAt };
11703
+ }
11704
+ if (existing.fingerprint !== fingerprint) {
11705
+ existing.fingerprint = fingerprint;
11706
+ existing.updatedAt = now;
11707
+ }
11708
+ return { createdAt: existing.createdAt, updatedAt: existing.updatedAt };
11709
+ }
11710
+ /** Drop a jobId (e.g. after cronDelete). Safe to call for unknown ids. */
11711
+ forget(jobId) {
11712
+ this.records.delete(jobId);
11713
+ }
11714
+ /** Test helper — read internal state without mutating it. */
11715
+ peek(jobId) {
11716
+ const r = this.records.get(jobId);
11717
+ return r ? { createdAt: r.createdAt, updatedAt: r.updatedAt } : void 0;
11718
+ }
11719
+ };
11671
11720
  function reverseResolveAgentName(cohortName, forwardMap) {
11672
11721
  const lower = cohortName.toLowerCase();
11673
11722
  for (const [openclawId, name] of Object.entries(forwardMap)) {
@@ -11692,7 +11741,7 @@ function checkRateLimit() {
11692
11741
  return true;
11693
11742
  }
11694
11743
  var MAX_CRON_MESSAGE_LENGTH = 1e3;
11695
- async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
11744
+ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger, cronTimestampTracker) {
11696
11745
  logger.info(`cohort-sync: executing command type=${cmd.type} id=${cmd._id}`);
11697
11746
  if (!checkRateLimit()) {
11698
11747
  logger.warn(`cohort-sync: rate limit exceeded (>${RATE_LIMIT_MAX} commands/min), rejecting command ${cmd._id}`);
@@ -11773,11 +11822,14 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
11773
11822
  default:
11774
11823
  logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
11775
11824
  }
11825
+ if (cmd.type === "cronDelete" && cmd.payload?.jobId && cronTimestampTracker) {
11826
+ cronTimestampTracker.forget(cmd.payload.jobId);
11827
+ }
11776
11828
  if (gwClient.isAlive()) {
11777
11829
  try {
11778
11830
  const snapResult = await gwClient.request("cron.list", { includeDisabled: true });
11779
11831
  const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
11780
- const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
11832
+ const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName, cronTimestampTracker));
11781
11833
  await pushCronSnapshot(cfg.apiKey, mapped);
11782
11834
  } catch (snapErr) {
11783
11835
  logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
@@ -11881,6 +11933,7 @@ var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTel
11881
11933
  var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
11882
11934
  var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
11883
11935
  var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
11936
+ var recordCronRunFromPluginRef = makeFunctionReference("cronRunHistory:recordFromPlugin");
11884
11937
  var getUndeliveredForPlugin = makeFunctionReference("notifications:getUndeliveredForPlugin");
11885
11938
  var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredByPlugin");
11886
11939
  var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
@@ -11946,6 +11999,25 @@ async function pushCronSnapshot(apiKey2, jobs) {
11946
11999
  return false;
11947
12000
  }
11948
12001
  }
12002
+ async function recordCronRun(apiKey2, run) {
12003
+ if (authCircuitOpen) return false;
12004
+ const c = getClient();
12005
+ if (!c) return false;
12006
+ try {
12007
+ await c.mutation(recordCronRunFromPluginRef, {
12008
+ apiKeyHash: hashApiKey(apiKey2),
12009
+ ...run
12010
+ });
12011
+ return true;
12012
+ } catch (err) {
12013
+ if (isUnauthorizedError(err)) {
12014
+ tripAuthCircuit();
12015
+ return false;
12016
+ }
12017
+ getLogger().error(`cohort-sync: recordCronRun failed: ${err}`);
12018
+ return false;
12019
+ }
12020
+ }
11949
12021
  async function callAddCommentFromPlugin(apiKey2, args) {
11950
12022
  if (authCircuitOpen) {
11951
12023
  throw new Error(
@@ -12169,7 +12241,7 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger, gwCl
12169
12241
  unsubscribers.push(unsubscribe);
12170
12242
  }
12171
12243
  }
12172
- function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12244
+ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient, cronTimestampTracker) {
12173
12245
  const c = getClient();
12174
12246
  if (!c) {
12175
12247
  logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
@@ -12192,7 +12264,7 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12192
12264
  commandId: cmd._id,
12193
12265
  apiKeyHash
12194
12266
  });
12195
- await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
12267
+ await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger, cronTimestampTracker);
12196
12268
  if (cmd.type === "restart") return;
12197
12269
  } catch (err) {
12198
12270
  if (isUnauthorizedError(err)) {
@@ -13487,12 +13559,12 @@ function dumpCtx(ctx) {
13487
13559
  function dumpEvent(event) {
13488
13560
  return dumpCtx(event);
13489
13561
  }
13490
- var PLUGIN_VERSION = true ? "0.29.0" : "unknown";
13562
+ var PLUGIN_VERSION = true ? "0.31.0" : "unknown";
13491
13563
  function resolveGatewayToken(api) {
13492
13564
  const token = api.config?.gateway?.auth?.token;
13493
13565
  return typeof token === "string" ? token : null;
13494
13566
  }
13495
- function registerCronEventHandlers(client2, cfg, resolveAgentName, logger) {
13567
+ function registerCronEventHandlers(client2, cfg, resolveAgentName, cronTimestampTracker, logger) {
13496
13568
  if (client2.availableEvents.has("cron")) {
13497
13569
  let debounceTimer = null;
13498
13570
  client2.on("cron", () => {
@@ -13501,7 +13573,7 @@ function registerCronEventHandlers(client2, cfg, resolveAgentName, logger) {
13501
13573
  try {
13502
13574
  const cronResult = await client2.request("cron.list", { includeDisabled: true });
13503
13575
  const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13504
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13576
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName, cronTimestampTracker));
13505
13577
  await pushCronSnapshot(cfg.apiKey, mapped);
13506
13578
  logger.debug("cohort-sync: cron event pushed", { count: mapped.length });
13507
13579
  } catch (err) {
@@ -13588,12 +13660,12 @@ function loadSessionsFromDisk(tracker, stateFilePath, logger) {
13588
13660
  } catch {
13589
13661
  }
13590
13662
  }
13591
- function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13663
+ function initGatewayClient(port, token, cfg, resolveAgentName, cronTimestampTracker, logger) {
13592
13664
  const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
13593
13665
  const onConnected = async () => {
13594
13666
  logger.debug(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
13595
13667
  logger.info(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
13596
- registerCronEventHandlers(client2, cfg, resolveAgentName, logger);
13668
+ registerCronEventHandlers(client2, cfg, resolveAgentName, cronTimestampTracker, logger);
13597
13669
  if (client2.availableEvents.has("shutdown")) {
13598
13670
  client2.on("shutdown", () => {
13599
13671
  logger.info("cohort-sync: gateway shutdown event received");
@@ -13602,7 +13674,7 @@ function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13602
13674
  try {
13603
13675
  const cronResult = await client2.request("cron.list", { includeDisabled: true });
13604
13676
  const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13605
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13677
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName, cronTimestampTracker));
13606
13678
  await pushCronSnapshot(cfg.apiKey, mapped);
13607
13679
  logger.debug("cohort-sync: cron snapshot pushed", { count: mapped.length });
13608
13680
  } catch (err) {
@@ -13641,6 +13713,19 @@ async function handleGatewayStart(event, state) {
13641
13713
  }
13642
13714
  } catch {
13643
13715
  }
13716
+ const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
13717
+ if (state.updateCheckInterval) clearInterval(state.updateCheckInterval);
13718
+ state.updateCheckInterval = setInterval(async () => {
13719
+ try {
13720
+ const latest = await checkForUpdate(PLUGIN_VERSION, logger);
13721
+ if (latest) state.latestPluginVersion = latest;
13722
+ } catch {
13723
+ }
13724
+ }, UPDATE_CHECK_INTERVAL_MS);
13725
+ if (typeof state.updateCheckInterval.unref === "function") {
13726
+ state.updateCheckInterval.unref();
13727
+ }
13728
+ logger.debug("cohort-sync: update check interval scheduled (24h)");
13644
13729
  try {
13645
13730
  const agentList = (config?.agents?.list ?? []).map((a) => ({
13646
13731
  id: a.id,
@@ -13666,14 +13751,14 @@ async function handleGatewayStart(event, state) {
13666
13751
  state.gatewayToken = token;
13667
13752
  logger.debug("cohort-sync: gateway client connecting", { port: event.port, hasToken: true });
13668
13753
  try {
13669
- const client2 = initGatewayClient(event.port, token, cfg, state.resolveAgentName, logger);
13754
+ const client2 = initGatewayClient(event.port, token, cfg, state.resolveAgentName, state.cronTimestampTracker, logger);
13670
13755
  state.persistentGwClient = client2;
13671
13756
  state.gwClientInitialized = true;
13672
13757
  if (state.commandUnsubscriber) {
13673
13758
  state.commandUnsubscriber();
13674
13759
  state.commandUnsubscriber = null;
13675
13760
  }
13676
- const unsub = startCommandSubscription(cfg, logger, state.resolveAgentName, state.persistentGwClient);
13761
+ const unsub = startCommandSubscription(cfg, logger, state.resolveAgentName, state.persistentGwClient, state.cronTimestampTracker);
13677
13762
  state.commandUnsubscriber = unsub;
13678
13763
  } catch (err) {
13679
13764
  logger.debug("cohort-sync: gateway client connect failed", { error: String(err) });
@@ -13837,16 +13922,40 @@ function registerHookHandlers(api, logger, getState) {
13837
13922
  }
13838
13923
  const sessionKey = ctx.sessionKey;
13839
13924
  if (sessionKey && sessionKey.includes(":cron:")) {
13925
+ const parsed = parseSessionKey(sessionKey);
13926
+ const routineId = parsed.kind === "cron" ? parsed.identifier : void 0;
13840
13927
  try {
13841
13928
  const raw = fs2["read"+"FileSync"](state.cronStorePath, "utf8");
13842
13929
  const store = JSON.parse(raw);
13843
13930
  const jobs = store.jobs ?? [];
13844
- const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName));
13931
+ const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName, state.cronTimestampTracker));
13845
13932
  await pushCronSnapshot(cfg.apiKey, mapped);
13846
13933
  log.debug("cohort-sync: cron agent end push", { count: mapped.length, sessionKey });
13847
13934
  } catch (err) {
13848
13935
  log.debug("cohort-sync: cron agent end push failed", { error: String(err) });
13849
13936
  }
13937
+ if (routineId) {
13938
+ const finishedAt = Date.now();
13939
+ const startedAtFromTracker = state.cronRunStarts.get(sessionKey);
13940
+ state.cronRunStarts.delete(sessionKey);
13941
+ const durationMs = typeof event?.durationMs === "number" && event.durationMs >= 0 ? event.durationMs : startedAtFromTracker != null ? Math.max(0, finishedAt - startedAtFromTracker) : void 0;
13942
+ const startedAt = startedAtFromTracker ?? (typeof durationMs === "number" ? finishedAt - durationMs : finishedAt);
13943
+ const status = event?.success === false ? "error" : "ok";
13944
+ const errorMessage = status === "error" && typeof event?.error === "string" ? event.error : void 0;
13945
+ try {
13946
+ await recordCronRun(cfg.apiKey, {
13947
+ routineId,
13948
+ startedAt,
13949
+ finishedAt,
13950
+ status,
13951
+ durationMs,
13952
+ ...errorMessage ? { errorMessage } : {}
13953
+ });
13954
+ log.debug("cohort-sync: cron run recorded", { routineId, status, durationMs });
13955
+ } catch (err) {
13956
+ log.debug("cohort-sync: cron run record failed", { error: String(err) });
13957
+ }
13958
+ }
13850
13959
  }
13851
13960
  if (event.success === false) {
13852
13961
  const entry = buildActivityEntry(agentName, "agent_end", {
@@ -13973,14 +14082,14 @@ function registerHookHandlers(api, logger, getState) {
13973
14082
  try {
13974
14083
  if (!state.gwClientInitialized && state.gatewayPort && state.gatewayToken) {
13975
14084
  try {
13976
- const client2 = initGatewayClient(state.gatewayPort, state.gatewayToken, cfg, state.resolveAgentName, log);
14085
+ const client2 = initGatewayClient(state.gatewayPort, state.gatewayToken, cfg, state.resolveAgentName, state.cronTimestampTracker, log);
13977
14086
  state.persistentGwClient = client2;
13978
14087
  state.gwClientInitialized = true;
13979
14088
  if (state.commandUnsubscriber) {
13980
14089
  state.commandUnsubscriber();
13981
14090
  state.commandUnsubscriber = null;
13982
14091
  }
13983
- const unsub = startCommandSubscription(cfg, log, state.resolveAgentName, state.persistentGwClient);
14092
+ const unsub = startCommandSubscription(cfg, log, state.resolveAgentName, state.persistentGwClient, state.cronTimestampTracker);
13984
14093
  state.commandUnsubscriber = unsub;
13985
14094
  } catch (err) {
13986
14095
  log.debug("cohort-sync: gateway client lazy init failed", { error: String(err) });
@@ -14007,6 +14116,9 @@ function registerHookHandlers(api, logger, getState) {
14007
14116
  } else if (sessionKey) {
14008
14117
  tracker.setSessionAgent(sessionKey, agentName);
14009
14118
  }
14119
+ if (sessionKey && sessionKey.includes(":cron:")) {
14120
+ state.cronRunStarts.set(sessionKey, Date.now());
14121
+ }
14010
14122
  const ctxChannelId = ctx.channelId;
14011
14123
  if (ctxChannelId) {
14012
14124
  setChannelAgent(ctxChannelId, agentName);
@@ -14191,6 +14303,10 @@ function registerHookHandlers(api, logger, getState) {
14191
14303
  clearInterval(state.keepaliveInterval);
14192
14304
  state.keepaliveInterval = null;
14193
14305
  }
14306
+ if (state.updateCheckInterval) {
14307
+ clearInterval(state.updateCheckInterval);
14308
+ state.updateCheckInterval = null;
14309
+ }
14194
14310
  if (state.channelsUnsubscriber) {
14195
14311
  try {
14196
14312
  state.channelsUnsubscriber();
@@ -14289,14 +14405,16 @@ function initializeHookState(api, cfg) {
14289
14405
  });
14290
14406
  const gatewayPort = api.config?.gateway?.port ?? null;
14291
14407
  const gatewayToken = resolveGatewayToken(api);
14408
+ const cronTimestampTracker = new CronTimestampTracker();
14409
+ const cronRunStarts = /* @__PURE__ */ new Map();
14292
14410
  let persistentGwClient = null;
14293
14411
  let gwClientInitialized = false;
14294
14412
  if (gatewayPort && gatewayToken) {
14295
- const client2 = initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
14413
+ const client2 = initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, cronTimestampTracker, logger);
14296
14414
  persistentGwClient = client2;
14297
14415
  gwClientInitialized = true;
14298
14416
  }
14299
- const commandUnsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
14417
+ const commandUnsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient, cronTimestampTracker);
14300
14418
  setToolRuntime({
14301
14419
  apiKey: cfg.apiKey,
14302
14420
  apiUrl: cfg.apiUrl,
@@ -14331,6 +14449,8 @@ function initializeHookState(api, cfg) {
14331
14449
  return {
14332
14450
  cfg,
14333
14451
  tracker,
14452
+ cronTimestampTracker,
14453
+ cronRunStarts,
14334
14454
  logger,
14335
14455
  config,
14336
14456
  resolveAgentName,
@@ -14347,7 +14467,8 @@ function initializeHookState(api, cfg) {
14347
14467
  commandUnsubscriber: commandUnsub,
14348
14468
  channelsUnsubscriber: null,
14349
14469
  api,
14350
- latestPluginVersion: null
14470
+ latestPluginVersion: null,
14471
+ updateCheckInterval: null
14351
14472
  };
14352
14473
  }
14353
14474
 
@@ -72,5 +72,5 @@
72
72
  }
73
73
  }
74
74
  },
75
- "version": "0.29.0"
75
+ "version": "0.31.0"
76
76
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",