@cfio/cohort-sync 0.28.0 → 0.30.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(
@@ -11975,6 +12047,13 @@ var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
11975
12047
  - Does your planned response address the task's stated scope? If not, do not comment.
11976
12048
  - Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
11977
12049
  - If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
12050
+ var ATMENTION_RESPONSE_PROMPT = `YOU WERE DIRECTLY @-MENTIONED. RESPOND.
12051
+
12052
+ - Use the cohort_comment tool to post a reply. Do NOT just think silently and exit.
12053
+ - Reply in your own voice (see your persona in IDENTITY.md).
12054
+ - If you need more context, fetch it via cohort_task before replying \u2014 then reply with what you found and your next step.
12055
+ - A brief, honest reply is better than no reply. If you genuinely have nothing to add, say so explicitly in a comment \u2014 don't go silent.
12056
+ - If the mention is a question, answer it. If it's a request, acknowledge what you'll do (and then do it).`;
11978
12057
  var TOOLS_REFERENCE = `
11979
12058
  TOOLS: Use these \u2014 do NOT call the REST API directly.
11980
12059
  - cohort_comment(task_number, comment) \u2014 post a comment
@@ -12035,9 +12114,16 @@ Comment: "${n.preview}"`;
12035
12114
 
12036
12115
  Scope: ${truncated}`;
12037
12116
  }
12038
- const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
12117
+ let prompt;
12118
+ if (n.type === "comment" && n.isMentioned) {
12119
+ prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
12120
+
12121
+ ${ATMENTION_RESPONSE_PROMPT}` : ATMENTION_RESPONSE_PROMPT;
12122
+ } else {
12123
+ prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
12039
12124
 
12040
12125
  ${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
12126
+ }
12041
12127
  const promptBlock = n.type === "comment" ? `
12042
12128
 
12043
12129
  ---
@@ -12155,7 +12241,7 @@ async function startNotificationSubscription(port, cfg, hooksToken, logger, gwCl
12155
12241
  unsubscribers.push(unsubscribe);
12156
12242
  }
12157
12243
  }
12158
- function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12244
+ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient, cronTimestampTracker) {
12159
12245
  const c = getClient();
12160
12246
  if (!c) {
12161
12247
  logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
@@ -12178,7 +12264,7 @@ function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
12178
12264
  commandId: cmd._id,
12179
12265
  apiKeyHash
12180
12266
  });
12181
- await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
12267
+ await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger, cronTimestampTracker);
12182
12268
  if (cmd.type === "restart") return;
12183
12269
  } catch (err) {
12184
12270
  if (isUnauthorizedError(err)) {
@@ -13473,12 +13559,12 @@ function dumpCtx(ctx) {
13473
13559
  function dumpEvent(event) {
13474
13560
  return dumpCtx(event);
13475
13561
  }
13476
- var PLUGIN_VERSION = true ? "0.28.0" : "unknown";
13562
+ var PLUGIN_VERSION = true ? "0.30.0" : "unknown";
13477
13563
  function resolveGatewayToken(api) {
13478
13564
  const token = api.config?.gateway?.auth?.token;
13479
13565
  return typeof token === "string" ? token : null;
13480
13566
  }
13481
- function registerCronEventHandlers(client2, cfg, resolveAgentName, logger) {
13567
+ function registerCronEventHandlers(client2, cfg, resolveAgentName, cronTimestampTracker, logger) {
13482
13568
  if (client2.availableEvents.has("cron")) {
13483
13569
  let debounceTimer = null;
13484
13570
  client2.on("cron", () => {
@@ -13487,7 +13573,7 @@ function registerCronEventHandlers(client2, cfg, resolveAgentName, logger) {
13487
13573
  try {
13488
13574
  const cronResult = await client2.request("cron.list", { includeDisabled: true });
13489
13575
  const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13490
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13576
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName, cronTimestampTracker));
13491
13577
  await pushCronSnapshot(cfg.apiKey, mapped);
13492
13578
  logger.debug("cohort-sync: cron event pushed", { count: mapped.length });
13493
13579
  } catch (err) {
@@ -13574,12 +13660,12 @@ function loadSessionsFromDisk(tracker, stateFilePath, logger) {
13574
13660
  } catch {
13575
13661
  }
13576
13662
  }
13577
- function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13663
+ function initGatewayClient(port, token, cfg, resolveAgentName, cronTimestampTracker, logger) {
13578
13664
  const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
13579
13665
  const onConnected = async () => {
13580
13666
  logger.debug(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
13581
13667
  logger.info(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
13582
- registerCronEventHandlers(client2, cfg, resolveAgentName, logger);
13668
+ registerCronEventHandlers(client2, cfg, resolveAgentName, cronTimestampTracker, logger);
13583
13669
  if (client2.availableEvents.has("shutdown")) {
13584
13670
  client2.on("shutdown", () => {
13585
13671
  logger.info("cohort-sync: gateway shutdown event received");
@@ -13588,7 +13674,7 @@ function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13588
13674
  try {
13589
13675
  const cronResult = await client2.request("cron.list", { includeDisabled: true });
13590
13676
  const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
13591
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13677
+ const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName, cronTimestampTracker));
13592
13678
  await pushCronSnapshot(cfg.apiKey, mapped);
13593
13679
  logger.debug("cohort-sync: cron snapshot pushed", { count: mapped.length });
13594
13680
  } catch (err) {
@@ -13652,14 +13738,14 @@ async function handleGatewayStart(event, state) {
13652
13738
  state.gatewayToken = token;
13653
13739
  logger.debug("cohort-sync: gateway client connecting", { port: event.port, hasToken: true });
13654
13740
  try {
13655
- const client2 = initGatewayClient(event.port, token, cfg, state.resolveAgentName, logger);
13741
+ const client2 = initGatewayClient(event.port, token, cfg, state.resolveAgentName, state.cronTimestampTracker, logger);
13656
13742
  state.persistentGwClient = client2;
13657
13743
  state.gwClientInitialized = true;
13658
13744
  if (state.commandUnsubscriber) {
13659
13745
  state.commandUnsubscriber();
13660
13746
  state.commandUnsubscriber = null;
13661
13747
  }
13662
- const unsub = startCommandSubscription(cfg, logger, state.resolveAgentName, state.persistentGwClient);
13748
+ const unsub = startCommandSubscription(cfg, logger, state.resolveAgentName, state.persistentGwClient, state.cronTimestampTracker);
13663
13749
  state.commandUnsubscriber = unsub;
13664
13750
  } catch (err) {
13665
13751
  logger.debug("cohort-sync: gateway client connect failed", { error: String(err) });
@@ -13823,16 +13909,40 @@ function registerHookHandlers(api, logger, getState) {
13823
13909
  }
13824
13910
  const sessionKey = ctx.sessionKey;
13825
13911
  if (sessionKey && sessionKey.includes(":cron:")) {
13912
+ const parsed = parseSessionKey(sessionKey);
13913
+ const routineId = parsed.kind === "cron" ? parsed.identifier : void 0;
13826
13914
  try {
13827
13915
  const raw = fs2["read"+"FileSync"](state.cronStorePath, "utf8");
13828
13916
  const store = JSON.parse(raw);
13829
13917
  const jobs = store.jobs ?? [];
13830
- const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName));
13918
+ const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName, state.cronTimestampTracker));
13831
13919
  await pushCronSnapshot(cfg.apiKey, mapped);
13832
13920
  log.debug("cohort-sync: cron agent end push", { count: mapped.length, sessionKey });
13833
13921
  } catch (err) {
13834
13922
  log.debug("cohort-sync: cron agent end push failed", { error: String(err) });
13835
13923
  }
13924
+ if (routineId) {
13925
+ const finishedAt = Date.now();
13926
+ const startedAtFromTracker = state.cronRunStarts.get(sessionKey);
13927
+ state.cronRunStarts.delete(sessionKey);
13928
+ const durationMs = typeof event?.durationMs === "number" && event.durationMs >= 0 ? event.durationMs : startedAtFromTracker != null ? Math.max(0, finishedAt - startedAtFromTracker) : void 0;
13929
+ const startedAt = startedAtFromTracker ?? (typeof durationMs === "number" ? finishedAt - durationMs : finishedAt);
13930
+ const status = event?.success === false ? "error" : "ok";
13931
+ const errorMessage = status === "error" && typeof event?.error === "string" ? event.error : void 0;
13932
+ try {
13933
+ await recordCronRun(cfg.apiKey, {
13934
+ routineId,
13935
+ startedAt,
13936
+ finishedAt,
13937
+ status,
13938
+ durationMs,
13939
+ ...errorMessage ? { errorMessage } : {}
13940
+ });
13941
+ log.debug("cohort-sync: cron run recorded", { routineId, status, durationMs });
13942
+ } catch (err) {
13943
+ log.debug("cohort-sync: cron run record failed", { error: String(err) });
13944
+ }
13945
+ }
13836
13946
  }
13837
13947
  if (event.success === false) {
13838
13948
  const entry = buildActivityEntry(agentName, "agent_end", {
@@ -13959,14 +14069,14 @@ function registerHookHandlers(api, logger, getState) {
13959
14069
  try {
13960
14070
  if (!state.gwClientInitialized && state.gatewayPort && state.gatewayToken) {
13961
14071
  try {
13962
- const client2 = initGatewayClient(state.gatewayPort, state.gatewayToken, cfg, state.resolveAgentName, log);
14072
+ const client2 = initGatewayClient(state.gatewayPort, state.gatewayToken, cfg, state.resolveAgentName, state.cronTimestampTracker, log);
13963
14073
  state.persistentGwClient = client2;
13964
14074
  state.gwClientInitialized = true;
13965
14075
  if (state.commandUnsubscriber) {
13966
14076
  state.commandUnsubscriber();
13967
14077
  state.commandUnsubscriber = null;
13968
14078
  }
13969
- const unsub = startCommandSubscription(cfg, log, state.resolveAgentName, state.persistentGwClient);
14079
+ const unsub = startCommandSubscription(cfg, log, state.resolveAgentName, state.persistentGwClient, state.cronTimestampTracker);
13970
14080
  state.commandUnsubscriber = unsub;
13971
14081
  } catch (err) {
13972
14082
  log.debug("cohort-sync: gateway client lazy init failed", { error: String(err) });
@@ -13993,6 +14103,9 @@ function registerHookHandlers(api, logger, getState) {
13993
14103
  } else if (sessionKey) {
13994
14104
  tracker.setSessionAgent(sessionKey, agentName);
13995
14105
  }
14106
+ if (sessionKey && sessionKey.includes(":cron:")) {
14107
+ state.cronRunStarts.set(sessionKey, Date.now());
14108
+ }
13996
14109
  const ctxChannelId = ctx.channelId;
13997
14110
  if (ctxChannelId) {
13998
14111
  setChannelAgent(ctxChannelId, agentName);
@@ -14275,14 +14388,16 @@ function initializeHookState(api, cfg) {
14275
14388
  });
14276
14389
  const gatewayPort = api.config?.gateway?.port ?? null;
14277
14390
  const gatewayToken = resolveGatewayToken(api);
14391
+ const cronTimestampTracker = new CronTimestampTracker();
14392
+ const cronRunStarts = /* @__PURE__ */ new Map();
14278
14393
  let persistentGwClient = null;
14279
14394
  let gwClientInitialized = false;
14280
14395
  if (gatewayPort && gatewayToken) {
14281
- const client2 = initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
14396
+ const client2 = initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, cronTimestampTracker, logger);
14282
14397
  persistentGwClient = client2;
14283
14398
  gwClientInitialized = true;
14284
14399
  }
14285
- const commandUnsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
14400
+ const commandUnsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient, cronTimestampTracker);
14286
14401
  setToolRuntime({
14287
14402
  apiKey: cfg.apiKey,
14288
14403
  apiUrl: cfg.apiUrl,
@@ -14317,6 +14432,8 @@ function initializeHookState(api, cfg) {
14317
14432
  return {
14318
14433
  cfg,
14319
14434
  tracker,
14435
+ cronTimestampTracker,
14436
+ cronRunStarts,
14320
14437
  logger,
14321
14438
  config,
14322
14439
  resolveAgentName,
@@ -72,5 +72,5 @@
72
72
  }
73
73
  }
74
74
  },
75
- "version": "0.28.0"
75
+ "version": "0.30.0"
76
76
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.28.0",
3
+ "version": "0.30.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.28.0",
3
+ "version": "0.30.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",