@cfio/cohort-sync 0.2.0 → 0.2.2

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
@@ -11568,23 +11568,35 @@ var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSess
11568
11568
  var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
11569
11569
  var HOT_KEY = "__cohort_sync__";
11570
11570
  function getHotState() {
11571
- return globalThis[HOT_KEY] ?? null;
11572
- }
11573
- function setHotState(state) {
11574
- globalThis[HOT_KEY] = state;
11571
+ let state = globalThis[HOT_KEY];
11572
+ if (!state) {
11573
+ state = {
11574
+ tracker: null,
11575
+ client: null,
11576
+ convexUrl: null,
11577
+ unsubscribers: [],
11578
+ lastKnownRoster: [],
11579
+ intervals: { heartbeat: null, activityFlush: null },
11580
+ activityBuffer: [],
11581
+ channelAgentBridge: {}
11582
+ };
11583
+ globalThis[HOT_KEY] = state;
11584
+ }
11585
+ if (!state.activityBuffer) state.activityBuffer = [];
11586
+ if (!state.intervals) state.intervals = { heartbeat: null, activityFlush: null };
11587
+ if (!state.channelAgentBridge) state.channelAgentBridge = {};
11588
+ if (!state.unsubscribers) state.unsubscribers = [];
11589
+ if (!state.lastKnownRoster) state.lastKnownRoster = [];
11590
+ return state;
11575
11591
  }
11576
11592
  function clearHotState() {
11577
11593
  delete globalThis[HOT_KEY];
11578
11594
  }
11579
11595
  function setRosterHotState(roster) {
11580
- const hot = getHotState();
11581
- if (hot) {
11582
- hot.lastKnownRoster = roster;
11583
- }
11596
+ getHotState().lastKnownRoster = roster;
11584
11597
  }
11585
11598
  function getRosterHotState() {
11586
- const hot = getHotState();
11587
- return hot?.lastKnownRoster ?? null;
11599
+ return getHotState().lastKnownRoster;
11588
11600
  }
11589
11601
  var savedLogger = null;
11590
11602
  function setLogger(logger) {
@@ -11601,51 +11613,54 @@ var client = null;
11601
11613
  var savedConvexUrl = null;
11602
11614
  var unsubscribers = [];
11603
11615
  function setConvexUrl(cfg) {
11604
- savedConvexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
11616
+ const url = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
11617
+ savedConvexUrl = url;
11618
+ getHotState().convexUrl = url;
11605
11619
  }
11606
11620
  function restoreFromHotReload(logger) {
11607
- const hot = getHotState();
11608
- if (!hot) return;
11609
- if (!client && hot.client) {
11610
- client = hot.client;
11611
- savedConvexUrl = hot.convexUrl;
11621
+ const state = getHotState();
11622
+ if (!client && state.client) {
11623
+ client = state.client;
11624
+ savedConvexUrl = state.convexUrl;
11612
11625
  logger.info("cohort-sync: recovered ConvexClient after hot-reload");
11613
11626
  }
11614
- if (unsubscribers.length === 0 && hot.unsubscribers.length > 0) {
11615
- unsubscribers.push(...hot.unsubscribers);
11616
- logger.info(`cohort-sync: recovered ${hot.unsubscribers.length} notification subscriptions after hot-reload`);
11627
+ if (unsubscribers.length === 0 && state.unsubscribers.length > 0) {
11628
+ unsubscribers.push(...state.unsubscribers);
11629
+ logger.info(`cohort-sync: recovered ${state.unsubscribers.length} notification subscriptions after hot-reload`);
11617
11630
  }
11618
11631
  }
11619
11632
  function getOrCreateClient() {
11620
11633
  if (client) return client;
11621
- const hot = getHotState();
11622
- if (hot?.client) {
11623
- client = hot.client;
11634
+ const state = getHotState();
11635
+ if (state.client) {
11636
+ client = state.client;
11624
11637
  getLogger().info("cohort-sync: recovered ConvexClient from globalThis");
11625
11638
  return client;
11626
11639
  }
11627
11640
  if (!savedConvexUrl) return null;
11628
11641
  client = new ConvexClient(savedConvexUrl);
11629
11642
  getLogger().info(`cohort-sync: created fresh ConvexClient (${savedConvexUrl})`);
11630
- setHotState({ client, convexUrl: savedConvexUrl, unsubscribers: [], lastKnownRoster: [] });
11643
+ state.client = client;
11644
+ state.convexUrl = savedConvexUrl;
11631
11645
  return client;
11632
11646
  }
11633
11647
  async function initSubscription(port, cfg, hooksToken, logger) {
11634
11648
  const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
11635
11649
  savedConvexUrl = convexUrl;
11636
- const hot = getHotState();
11637
- if (hot?.client) {
11638
- client = hot.client;
11650
+ const state = getHotState();
11651
+ if (state.client) {
11652
+ client = state.client;
11639
11653
  logger.info("cohort-sync: reusing hot-reload ConvexClient for subscription");
11640
11654
  } else {
11641
11655
  client = new ConvexClient(convexUrl);
11642
11656
  logger.info(`cohort-sync: created new ConvexClient for subscription (${convexUrl})`);
11643
11657
  }
11658
+ state.client = client;
11659
+ state.convexUrl = convexUrl;
11644
11660
  if (!hooksToken) {
11645
11661
  logger.warn(
11646
11662
  `cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
11647
11663
  );
11648
- setHotState({ client, convexUrl, unsubscribers: [], lastKnownRoster: [] });
11649
11664
  return;
11650
11665
  }
11651
11666
  const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
@@ -11688,7 +11703,7 @@ async function initSubscription(port, cfg, hooksToken, logger) {
11688
11703
  );
11689
11704
  unsubscribers.push(unsubscribe);
11690
11705
  }
11691
- setHotState({ client, convexUrl, unsubscribers: [...unsubscribers], lastKnownRoster: [] });
11706
+ state.unsubscribers = [...unsubscribers];
11692
11707
  }
11693
11708
  function closeSubscription() {
11694
11709
  for (const unsub of unsubscribers) {
@@ -11698,13 +11713,11 @@ function closeSubscription() {
11698
11713
  }
11699
11714
  }
11700
11715
  unsubscribers.length = 0;
11701
- const hot = getHotState();
11702
- if (hot?.unsubscribers) {
11703
- for (const unsub of hot.unsubscribers) {
11704
- try {
11705
- unsub();
11706
- } catch {
11707
- }
11716
+ const state = getHotState();
11717
+ for (const unsub of state.unsubscribers) {
11718
+ try {
11719
+ unsub();
11720
+ } catch {
11708
11721
  }
11709
11722
  }
11710
11723
  client?.close();
@@ -11739,14 +11752,35 @@ async function pushActivity(apiKey, entries) {
11739
11752
  getLogger().error(`cohort-sync: pushActivity failed: ${err}`);
11740
11753
  }
11741
11754
  }
11742
- function saveAgentStatesToHot(states) {
11743
- const hot = getHotState();
11744
- if (hot) {
11745
- hot.agentStates = states;
11755
+ function saveIntervalsToHot(heartbeat, activityFlush) {
11756
+ const state = getHotState();
11757
+ state.intervals = { heartbeat, activityFlush };
11758
+ }
11759
+ function clearIntervalsFromHot() {
11760
+ const state = getHotState();
11761
+ if (state.intervals.heartbeat) clearInterval(state.intervals.heartbeat);
11762
+ if (state.intervals.activityFlush) clearInterval(state.intervals.activityFlush);
11763
+ state.intervals = { heartbeat: null, activityFlush: null };
11764
+ }
11765
+ function addActivityToHot(entry) {
11766
+ const state = getHotState();
11767
+ state.activityBuffer.push(entry);
11768
+ const logger = savedLogger;
11769
+ if (logger) {
11770
+ logger.info(`cohort-sync: +activity [${entry.category}] "${entry.text}"`);
11746
11771
  }
11747
11772
  }
11748
- function getAgentStatesFromHot() {
11749
- return getHotState()?.agentStates ?? null;
11773
+ function drainActivityFromHot() {
11774
+ const state = getHotState();
11775
+ const buf = state.activityBuffer;
11776
+ state.activityBuffer = [];
11777
+ return buf;
11778
+ }
11779
+ function setChannelAgent(channelId, agentName) {
11780
+ getHotState().channelAgentBridge[channelId] = agentName;
11781
+ }
11782
+ function getChannelAgent(channelId) {
11783
+ return getHotState().channelAgentBridge[channelId] ?? null;
11750
11784
  }
11751
11785
 
11752
11786
  // src/sync.ts
@@ -11977,6 +12011,9 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
11977
12011
  logger.info("cohort-sync: full sync complete");
11978
12012
  }
11979
12013
 
12014
+ // src/agent-state.ts
12015
+ import { basename } from "node:path";
12016
+
11980
12017
  // src/session-key.ts
11981
12018
  function parseSessionKey(key) {
11982
12019
  if (!key || !key.includes(":")) {
@@ -12014,6 +12051,7 @@ function shouldPushTelemetry(current, lastPushed) {
12014
12051
  if (!lastPushed) return true;
12015
12052
  if (current.status !== lastPushed.status) return true;
12016
12053
  if (current.compactions !== lastPushed.compactions) return true;
12054
+ if (current.activeSessions !== lastPushed.activeSessions) return true;
12017
12055
  if (current.contextLimit > 0 && lastPushed.contextLimit > 0) {
12018
12056
  const currentPct = current.contextTokens / current.contextLimit;
12019
12057
  const lastPct = lastPushed.contextTokens / lastPushed.contextLimit;
@@ -12042,57 +12080,180 @@ function shouldPushSessions(current, lastPushed) {
12042
12080
  }
12043
12081
  return false;
12044
12082
  }
12045
- function basename(filePath) {
12046
- const parts = filePath.split("/");
12047
- return parts[parts.length - 1] ?? filePath;
12083
+ function summarizeError(error) {
12084
+ const str = String(error ?? "");
12085
+ const firstLine = str.split("\n")[0] ?? str;
12086
+ return firstLine.slice(0, 120);
12048
12087
  }
12049
- function buildActivitySentence(agentName, event, context) {
12088
+ function formatDuration(ms) {
12089
+ const seconds = Math.round(ms / 1e3);
12090
+ if (seconds < 60) return `${seconds}s`;
12091
+ const minutes = Math.round(seconds / 60);
12092
+ return `${minutes}m`;
12093
+ }
12094
+ function buildActivityEntry(agentName, hook, context) {
12050
12095
  const ts = Date.now();
12051
12096
  const name = agentName.charAt(0).toUpperCase() + agentName.slice(1);
12052
- switch (event) {
12053
- case "before_tool_call": {
12097
+ const sessionKey = context.sessionKey;
12098
+ const model = context.model;
12099
+ const channel = context.channel;
12100
+ switch (hook) {
12101
+ case "after_tool_call": {
12054
12102
  const toolName = String(context.toolName ?? "a tool");
12055
12103
  const params = context.params;
12056
- if (toolName === "Edit" || toolName === "Write") {
12057
- const file = params?.file_path ? basename(String(params.file_path)) : "a file";
12058
- return { agentName, text: `${name} is editing ${file}`, category: "tool", timestamp: ts };
12059
- }
12104
+ const error = context.error;
12105
+ const durationMs = context.durationMs;
12106
+ const hasError = error != null && error !== "";
12107
+ let text;
12060
12108
  if (toolName === "Read") {
12061
12109
  const file = params?.file_path ? basename(String(params.file_path)) : "a file";
12062
- return { agentName, text: `${name} is reading ${file}`, category: "tool", timestamp: ts };
12063
- }
12064
- if (toolName === "Bash") {
12065
- return { agentName, text: `${name} is running a command`, category: "tool", timestamp: ts };
12066
- }
12067
- if (toolName === "cohort_comment") {
12110
+ text = hasError ? `${name} failed to read ${file}: ${summarizeError(error)}` : `${name} read ${file}`;
12111
+ } else if (toolName === "Edit" || toolName === "Write") {
12112
+ const file = params?.file_path ? basename(String(params.file_path)) : "a file";
12113
+ text = hasError ? `${name} failed to edit ${file}: ${summarizeError(error)}` : `${name} edited ${file}`;
12114
+ } else if (toolName === "Bash") {
12115
+ text = hasError ? `${name} ran a command (failed: ${summarizeError(error)})` : `${name} ran a command`;
12116
+ } else if (toolName === "cohort_comment") {
12068
12117
  const taskNum = params?.task_number ?? "?";
12069
- return { agentName, text: `${name} is commenting on task #${taskNum}`, category: "tool", timestamp: ts };
12118
+ text = hasError ? `${name} failed to comment on task #${taskNum}: ${summarizeError(error)}` : `${name} commented on task #${taskNum}`;
12119
+ } else {
12120
+ text = hasError ? `${name} used ${toolName} (failed: ${summarizeError(error)})` : `${name} used ${toolName}`;
12070
12121
  }
12071
- return { agentName, text: `${name} used ${toolName}`, category: "tool", timestamp: ts };
12122
+ return {
12123
+ agentName,
12124
+ text,
12125
+ category: "tool",
12126
+ timestamp: ts,
12127
+ hook,
12128
+ toolName,
12129
+ sessionKey,
12130
+ model,
12131
+ durationMs,
12132
+ error: hasError ? true : void 0,
12133
+ errorSummary: hasError ? summarizeError(error) : void 0
12134
+ };
12072
12135
  }
12073
12136
  case "message_received": {
12074
- const ch = String(context.channel ?? context.messageProvider ?? "unknown");
12075
- return { agentName, text: `${name} received a message on ${ch}`, category: "message", timestamp: ts };
12137
+ const ch = String(channel ?? "unknown");
12138
+ return {
12139
+ agentName,
12140
+ text: `${name} received a message on ${ch}`,
12141
+ category: "message",
12142
+ timestamp: ts,
12143
+ hook,
12144
+ channel: ch,
12145
+ sessionKey,
12146
+ model
12147
+ };
12076
12148
  }
12077
12149
  case "message_sent": {
12078
- const ch = String(context.channel ?? context.messageProvider ?? "unknown");
12079
- return { agentName, text: `${name} sent a reply on ${ch}`, category: "message", timestamp: ts };
12080
- }
12081
- case "after_compaction": {
12082
- const before = context.beforePercent != null ? `${context.beforePercent}%` : "?";
12083
- const after = context.afterPercent != null ? `${context.afterPercent}%` : "?";
12084
- return { agentName, text: `${name} context compacted \u2014 ${before} \u2192 ${after}`, category: "system", timestamp: ts };
12150
+ const ch = String(channel ?? "unknown");
12151
+ const success = context.success !== false;
12152
+ const error = context.error;
12153
+ const text = success ? `${name} sent a reply on ${ch}` : `${name} failed to send on ${ch}: ${summarizeError(error)}`;
12154
+ return {
12155
+ agentName,
12156
+ text,
12157
+ category: "message",
12158
+ timestamp: ts,
12159
+ hook,
12160
+ channel: ch,
12161
+ sessionKey,
12162
+ model,
12163
+ error: !success ? true : void 0,
12164
+ errorSummary: !success ? summarizeError(error) : void 0
12165
+ };
12085
12166
  }
12086
12167
  case "session_start": {
12087
- const ch = String(context.channel ?? "unknown");
12088
- return { agentName, text: `${name} started a session on ${ch}`, category: "session", timestamp: ts };
12168
+ const ch = String(channel ?? "unknown");
12169
+ const resumed = context.resumedFrom != null;
12170
+ const text = resumed ? `${name} resumed a session` : `${name} started a session on ${ch}`;
12171
+ return {
12172
+ agentName,
12173
+ text,
12174
+ category: "session",
12175
+ timestamp: ts,
12176
+ hook,
12177
+ channel: ch,
12178
+ sessionKey,
12179
+ model
12180
+ };
12089
12181
  }
12090
12182
  case "session_end": {
12091
- return { agentName, text: `${name} ended a session`, category: "session", timestamp: ts };
12183
+ const msgCount = context.messageCount;
12184
+ const durationMs = context.durationMs;
12185
+ const parts = [];
12186
+ if (msgCount != null) parts.push(`${msgCount} messages`);
12187
+ if (durationMs != null) parts.push(formatDuration(durationMs));
12188
+ const stats = parts.length > 0 ? ` (${parts.join(", ")})` : "";
12189
+ return {
12190
+ agentName,
12191
+ text: `${name} ended a session${stats}`,
12192
+ category: "session",
12193
+ timestamp: ts,
12194
+ hook,
12195
+ sessionKey,
12196
+ model,
12197
+ durationMs
12198
+ };
12199
+ }
12200
+ case "before_compaction": {
12201
+ return {
12202
+ agentName,
12203
+ text: `${name} context compaction starting`,
12204
+ category: "system",
12205
+ timestamp: ts,
12206
+ hook,
12207
+ sessionKey,
12208
+ model
12209
+ };
12092
12210
  }
12093
- case "before_agent_start": {
12094
- const ch = String(context.channel ?? "unknown");
12095
- return { agentName, text: `${name} is working on ${ch}`, category: "session", timestamp: ts };
12211
+ case "after_compaction": {
12212
+ const compactedCount = context.compactedCount;
12213
+ const messageCount = context.messageCount;
12214
+ const parts = [];
12215
+ if (compactedCount != null) parts.push(`${compactedCount} messages removed`);
12216
+ if (messageCount != null) parts.push(`${messageCount} remaining`);
12217
+ const stats = parts.length > 0 ? ` (${parts.join(", ")})` : "";
12218
+ return {
12219
+ agentName,
12220
+ text: `${name} context compacted${stats}`,
12221
+ category: "system",
12222
+ timestamp: ts,
12223
+ hook,
12224
+ sessionKey,
12225
+ model
12226
+ };
12227
+ }
12228
+ case "agent_end": {
12229
+ if (context.success !== false) return null;
12230
+ const error = context.error;
12231
+ const durationMs = context.durationMs;
12232
+ return {
12233
+ agentName,
12234
+ text: `${name} processing failed: ${summarizeError(error)}`,
12235
+ category: "system",
12236
+ timestamp: ts,
12237
+ hook,
12238
+ sessionKey,
12239
+ model,
12240
+ durationMs,
12241
+ error: true,
12242
+ errorSummary: summarizeError(error)
12243
+ };
12244
+ }
12245
+ case "before_reset": {
12246
+ const reason = context.reason;
12247
+ const text = reason ? `${name} session was reset (${reason})` : `${name} session was reset`;
12248
+ return {
12249
+ agentName,
12250
+ text,
12251
+ category: "system",
12252
+ timestamp: ts,
12253
+ hook,
12254
+ sessionKey,
12255
+ model
12256
+ };
12096
12257
  }
12097
12258
  default:
12098
12259
  return null;
@@ -12101,6 +12262,13 @@ function buildActivitySentence(agentName, event, context) {
12101
12262
  var AgentStateTracker = class {
12102
12263
  agents = /* @__PURE__ */ new Map();
12103
12264
  activityBuffer = [];
12265
+ sessionKeyToAgent = /* @__PURE__ */ new Map();
12266
+ setSessionAgent(sessionKey, agentName) {
12267
+ this.sessionKeyToAgent.set(sessionKey, agentName);
12268
+ }
12269
+ getSessionAgent(sessionKey) {
12270
+ return this.sessionKeyToAgent.get(sessionKey) ?? null;
12271
+ }
12104
12272
  getOrCreate(agentName) {
12105
12273
  let state = this.agents.get(agentName);
12106
12274
  if (!state) {
@@ -12142,7 +12310,29 @@ var AgentStateTracker = class {
12142
12310
  state.sessions.delete(sessionKey);
12143
12311
  }
12144
12312
  }
12313
+ hasSession(agentName, sessionKey) {
12314
+ const state = this.agents.get(agentName);
12315
+ if (!state) return false;
12316
+ return state.sessions.has(sessionKey);
12317
+ }
12318
+ pruneStaleSessions(agentName, maxAgeMs) {
12319
+ const state = this.agents.get(agentName);
12320
+ if (!state) return [];
12321
+ const now = Date.now();
12322
+ const pruned = [];
12323
+ for (const [key, session] of state.sessions) {
12324
+ const lastMs = new Date(session.lastActivity).getTime();
12325
+ if (now - lastMs > maxAgeMs) {
12326
+ state.sessions.delete(key);
12327
+ pruned.push(key);
12328
+ }
12329
+ }
12330
+ return pruned;
12331
+ }
12145
12332
  // --- Telemetry updates ---
12333
+ getStatus(agentName) {
12334
+ return this.agents.get(agentName)?.status ?? null;
12335
+ }
12146
12336
  updateStatus(agentName, status) {
12147
12337
  this.getOrCreate(agentName).status = status;
12148
12338
  }
@@ -12243,21 +12433,72 @@ var AgentStateTracker = class {
12243
12433
  clear() {
12244
12434
  this.agents.clear();
12245
12435
  this.activityBuffer = [];
12436
+ this.sessionKeyToAgent.clear();
12246
12437
  }
12247
12438
  exportState() {
12248
- return new Map(this.agents);
12439
+ return {
12440
+ agents: new Map(this.agents),
12441
+ sessionKeyToAgent: new Map(this.sessionKeyToAgent),
12442
+ activityBuffer: [...this.activityBuffer]
12443
+ };
12249
12444
  }
12250
12445
  importState(states) {
12251
- for (const [name, state] of states) {
12446
+ const agentMap = states instanceof Map ? states : states.agents;
12447
+ for (const [name, state] of agentMap) {
12252
12448
  if (!(state.sessions instanceof Map)) {
12253
12449
  state.sessions = new Map(Object.entries(state.sessions));
12254
12450
  }
12255
12451
  this.agents.set(name, state);
12256
12452
  }
12453
+ if (!(states instanceof Map) && states.activityBuffer) {
12454
+ this.activityBuffer.push(...states.activityBuffer);
12455
+ }
12456
+ if (!(states instanceof Map) && states.sessionKeyToAgent) {
12457
+ for (const [key, agent] of states.sessionKeyToAgent) {
12458
+ this.sessionKeyToAgent.set(key, agent);
12459
+ }
12460
+ }
12257
12461
  }
12258
12462
  };
12259
12463
 
12260
12464
  // src/hooks.ts
12465
+ var BUILD_ID = "B9-ACCOUNTID-20260311";
12466
+ var DIAG_LOG_PATH = "/tmp/cohort-sync-diag.log";
12467
+ function diag(label, data) {
12468
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
12469
+ const payload = data ? " " + JSON.stringify(data, (_k, v2) => {
12470
+ if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
12471
+ return v2;
12472
+ }) : "";
12473
+ const line = `[${ts}] ${label}${payload}
12474
+ `;
12475
+ try {
12476
+ fs.appendFileSync(DIAG_LOG_PATH, line);
12477
+ } catch {
12478
+ }
12479
+ }
12480
+ function dumpCtx(ctx) {
12481
+ if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
12482
+ const out = {};
12483
+ for (const key of Object.keys(ctx)) {
12484
+ const val = ctx[key];
12485
+ if (typeof val === "function") {
12486
+ out[key] = "[Function]";
12487
+ } else if (typeof val === "object" && val !== null) {
12488
+ try {
12489
+ out[key] = JSON.parse(JSON.stringify(val));
12490
+ } catch {
12491
+ out[key] = "[Object]";
12492
+ }
12493
+ } else {
12494
+ out[key] = val;
12495
+ }
12496
+ }
12497
+ return out;
12498
+ }
12499
+ function dumpEvent(event) {
12500
+ return dumpCtx(event);
12501
+ }
12261
12502
  var PLUGIN_VERSION = "unknown";
12262
12503
  try {
12263
12504
  const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "package.json");
@@ -12265,6 +12506,7 @@ try {
12265
12506
  PLUGIN_VERSION = pkgJson.version ?? "unknown";
12266
12507
  } catch {
12267
12508
  }
12509
+ diag("MODULE_LOADED", { BUILD_ID, PLUGIN_VERSION });
12268
12510
  function parseIdentityFile(workspaceDir) {
12269
12511
  try {
12270
12512
  const filePath = path.join(workspaceDir, "IDENTITY.md");
@@ -12297,24 +12539,189 @@ function resolveIdentity(configIdentity, workspaceDir) {
12297
12539
  avatar: configIdentity?.avatar ?? fileIdentity?.avatar
12298
12540
  };
12299
12541
  }
12542
+ function getOrCreateTracker() {
12543
+ const state = getHotState();
12544
+ if (state.tracker instanceof AgentStateTracker) {
12545
+ return state.tracker;
12546
+ }
12547
+ const fresh = new AgentStateTracker();
12548
+ state.tracker = fresh;
12549
+ return fresh;
12550
+ }
12551
+ var STATE_FILE_PATH = path.join(
12552
+ path.dirname(new URL(import.meta.url).pathname),
12553
+ ".session-state.json"
12554
+ );
12555
+ function saveSessionsToDisk(tracker) {
12556
+ try {
12557
+ const state = tracker.exportState();
12558
+ const data = {
12559
+ sessions: [],
12560
+ sessionKeyToAgent: Object.fromEntries(state.sessionKeyToAgent),
12561
+ channelAgents: { ...globalThis["__cohort_sync_channel_agent__"] ?? {} },
12562
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
12563
+ };
12564
+ for (const [name, agent] of state.agents) {
12565
+ for (const key of agent.sessions.keys()) {
12566
+ data.sessions.push({ agentName: name, key });
12567
+ }
12568
+ }
12569
+ fs.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
12570
+ } catch {
12571
+ }
12572
+ }
12573
+ function loadSessionsFromDisk(tracker, logger) {
12574
+ try {
12575
+ if (!fs.existsSync(STATE_FILE_PATH)) return;
12576
+ const data = JSON.parse(fs.readFileSync(STATE_FILE_PATH, "utf8"));
12577
+ if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
12578
+ logger.info("cohort-sync: disk session state too old (>24h), skipping");
12579
+ return;
12580
+ }
12581
+ let count = 0;
12582
+ for (const { agentName, key } of data.sessions) {
12583
+ if (!tracker.hasSession(agentName, key)) {
12584
+ tracker.addSession(agentName, key);
12585
+ count++;
12586
+ }
12587
+ }
12588
+ for (const [key, agent] of Object.entries(data.sessionKeyToAgent)) {
12589
+ tracker.setSessionAgent(key, agent);
12590
+ }
12591
+ for (const [channelId, agent] of Object.entries(data.channelAgents ?? {})) {
12592
+ setChannelAgent(channelId, agent);
12593
+ }
12594
+ if (count > 0) {
12595
+ logger.info(`cohort-sync: restored ${count} sessions from disk`);
12596
+ }
12597
+ } catch {
12598
+ }
12599
+ }
12300
12600
  function registerHooks(api, cfg) {
12301
12601
  const { logger, config } = api;
12302
12602
  const nameMap = cfg.agentNameMap;
12303
- const tracker = new AgentStateTracker();
12603
+ const tracker = getOrCreateTracker();
12304
12604
  let heartbeatInterval = null;
12305
12605
  let activityFlushInterval = null;
12606
+ logger.info(`cohort-sync: registerHooks [${BUILD_ID}]`);
12607
+ diag("REGISTER_HOOKS", {
12608
+ BUILD_ID,
12609
+ PLUGIN_VERSION,
12610
+ hasNameMap: !!nameMap,
12611
+ nameMapKeys: nameMap ? Object.keys(nameMap) : [],
12612
+ nameMapValues: nameMap ? Object.values(nameMap) : [],
12613
+ agentCount: config?.agents?.list?.length ?? 0,
12614
+ agentIds: (config?.agents?.list ?? []).map((a) => a.id),
12615
+ agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
12616
+ });
12306
12617
  setConvexUrl(cfg);
12307
12618
  setLogger(logger);
12308
12619
  restoreFromHotReload(logger);
12309
12620
  restoreRosterFromHotReload(getRosterHotState(), logger);
12310
- const savedStates = getAgentStatesFromHot();
12311
- if (savedStates) {
12312
- tracker.importState(savedStates);
12313
- logger.info("cohort-sync: recovered AgentStateTracker state after hot-reload");
12621
+ if (tracker.getAgentNames().length === 0) {
12622
+ loadSessionsFromDisk(tracker, logger);
12623
+ const restoredAgents = tracker.getAgentNames();
12624
+ for (const agentName of restoredAgents) {
12625
+ const sessSnapshot = tracker.getSessionsSnapshot(agentName);
12626
+ if (sessSnapshot.length > 0) {
12627
+ pushSessions(cfg.apiKey, agentName, sessSnapshot).then(() => {
12628
+ tracker.markSessionsPushed(agentName);
12629
+ logger.info(`cohort-sync: pushed ${sessSnapshot.length} restored sessions for ${agentName}`);
12630
+ }).catch((err) => {
12631
+ logger.warn(`cohort-sync: failed to push restored sessions for ${agentName}: ${String(err)}`);
12632
+ });
12633
+ }
12634
+ }
12314
12635
  }
12315
12636
  function resolveAgentName(agentId) {
12316
12637
  return (nameMap?.[agentId] ?? agentId).toLowerCase();
12317
12638
  }
12639
+ function resolveAgentFromContext(ctx) {
12640
+ const allCtxKeys = Object.keys(ctx);
12641
+ diag("RESOLVE_AGENT_FROM_CTX_START", {
12642
+ ctxKeys: allCtxKeys,
12643
+ agentId: ctx.agentId,
12644
+ sessionKey: ctx.sessionKey,
12645
+ sessionId: ctx.sessionId,
12646
+ channelId: ctx.channelId,
12647
+ accountId: ctx.accountId,
12648
+ conversationId: ctx.conversationId,
12649
+ messageProvider: ctx.messageProvider,
12650
+ workspaceDir: ctx.workspaceDir
12651
+ });
12652
+ if (ctx.agentId && typeof ctx.agentId === "string") {
12653
+ const resolved2 = resolveAgentName(ctx.agentId);
12654
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "agentId", raw: ctx.agentId, resolved: resolved2 });
12655
+ return resolved2;
12656
+ }
12657
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
12658
+ if (sessionKey && typeof sessionKey === "string") {
12659
+ const mapped = tracker.getSessionAgent(sessionKey);
12660
+ if (mapped) {
12661
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "sessionKey_mapped", sessionKey, mapped });
12662
+ return mapped;
12663
+ }
12664
+ const parts = sessionKey.split(":");
12665
+ if (parts[0] === "agent" && parts.length >= 2) {
12666
+ const resolved2 = resolveAgentName(parts[1]);
12667
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "sessionKey_parsed", sessionKey, agentPart: parts[1], resolved: resolved2 });
12668
+ return resolved2;
12669
+ }
12670
+ }
12671
+ const accountId = ctx.accountId;
12672
+ if (accountId && typeof accountId === "string") {
12673
+ const resolved2 = resolveAgentName(accountId);
12674
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "accountId", accountId, resolved: resolved2 });
12675
+ return resolved2;
12676
+ }
12677
+ const channelId = ctx.channelId;
12678
+ if (channelId && typeof channelId === "string") {
12679
+ const channelAgent = getChannelAgent(channelId);
12680
+ if (channelAgent) {
12681
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_bridge", channelId, channelAgent, bridgeState: { ...getHotState().channelAgentBridge } });
12682
+ return channelAgent;
12683
+ }
12684
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_raw", channelId, bridgeState: { ...getHotState().channelAgentBridge } });
12685
+ return String(channelId);
12686
+ }
12687
+ const resolved = resolveAgentName("main");
12688
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "fallback_main", resolved });
12689
+ return resolved;
12690
+ }
12691
+ function resolveChannelFromContext(ctx) {
12692
+ if (ctx.messageProvider && typeof ctx.messageProvider === "string") {
12693
+ return ctx.messageProvider;
12694
+ }
12695
+ if (ctx.channelId && typeof ctx.channelId === "string") {
12696
+ return ctx.channelId;
12697
+ }
12698
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
12699
+ if (sessionKey && typeof sessionKey === "string") {
12700
+ const parsed = parseSessionKey(sessionKey);
12701
+ return parsed.channel;
12702
+ }
12703
+ return "unknown";
12704
+ }
12705
+ const pendingActivity = drainActivityFromHot();
12706
+ if (pendingActivity.length > 0) {
12707
+ pushActivity(cfg.apiKey, pendingActivity).catch((err) => {
12708
+ logger.warn(`cohort-sync: pre-reload activity flush failed: ${String(err)}`);
12709
+ });
12710
+ logger.info(`cohort-sync: flushed ${pendingActivity.length} pending activity entries before hot-reload`);
12711
+ }
12712
+ clearIntervalsFromHot();
12713
+ heartbeatInterval = setInterval(() => {
12714
+ pushHeartbeat().catch((err) => {
12715
+ logger.warn(`cohort-sync: heartbeat tick failed: ${String(err)}`);
12716
+ });
12717
+ }, 12e4);
12718
+ activityFlushInterval = setInterval(() => {
12719
+ flushActivityBuffer().catch((err) => {
12720
+ logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
12721
+ });
12722
+ }, 3e3);
12723
+ saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
12724
+ logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=3s)");
12318
12725
  api.registerTool((toolCtx) => {
12319
12726
  const agentId = toolCtx.agentId ?? "main";
12320
12727
  const agentName = resolveAgentName(agentId);
@@ -12361,6 +12768,13 @@ function registerHooks(api, cfg) {
12361
12768
  }
12362
12769
  async function pushHeartbeat() {
12363
12770
  const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
12771
+ for (const agentId of allAgentIds) {
12772
+ const agentName = resolveAgentName(agentId);
12773
+ const pruned = tracker.pruneStaleSessions(agentName, 864e5);
12774
+ if (pruned.length > 0) {
12775
+ logger.info(`cohort-sync: pruned ${pruned.length} stale sessions for ${agentName}`);
12776
+ }
12777
+ }
12364
12778
  for (const agentId of allAgentIds) {
12365
12779
  const agentName = resolveAgentName(agentId);
12366
12780
  try {
@@ -12374,10 +12788,23 @@ function registerHooks(api, cfg) {
12374
12788
  logger.warn(`cohort-sync: heartbeat push failed for ${agentName}: ${String(err)}`);
12375
12789
  }
12376
12790
  }
12791
+ for (const agentId of allAgentIds) {
12792
+ const agentName = resolveAgentName(agentId);
12793
+ try {
12794
+ if (tracker.shouldPushSessions(agentName)) {
12795
+ const sessSnapshot = tracker.getSessionsSnapshot(agentName);
12796
+ await pushSessions(cfg.apiKey, agentName, sessSnapshot);
12797
+ tracker.markSessionsPushed(agentName);
12798
+ }
12799
+ } catch (err) {
12800
+ logger.warn(`cohort-sync: heartbeat session push failed for ${agentName}: ${String(err)}`);
12801
+ }
12802
+ }
12803
+ saveSessionsToDisk(tracker);
12377
12804
  logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
12378
12805
  }
12379
12806
  async function flushActivityBuffer() {
12380
- const entries = tracker.flushActivity();
12807
+ const entries = drainActivityFromHot();
12381
12808
  if (entries.length === 0) return;
12382
12809
  try {
12383
12810
  await pushActivity(cfg.apiKey, entries);
@@ -12386,6 +12813,7 @@ function registerHooks(api, cfg) {
12386
12813
  }
12387
12814
  }
12388
12815
  api.on("gateway_start", async (event) => {
12816
+ diag("HOOK_gateway_start", { port: event.port, eventKeys: Object.keys(event) });
12389
12817
  try {
12390
12818
  checkForUpdate(PLUGIN_VERSION, logger).catch(() => {
12391
12819
  });
@@ -12401,6 +12829,15 @@ function registerHooks(api, cfg) {
12401
12829
  } catch (err) {
12402
12830
  logger.error(`cohort-sync: gateway_start sync failed: ${String(err)}`);
12403
12831
  }
12832
+ for (const a of config?.agents?.list ?? []) {
12833
+ const agentName = resolveAgentName(a.id);
12834
+ const mp = a.messageProvider;
12835
+ diag("GATEWAY_START_SEED_BRIDGE", { agentId: a.id, agentName, messageProvider: mp, accountId: a.accountId });
12836
+ if (mp && typeof mp === "string") {
12837
+ setChannelAgent(mp, agentName);
12838
+ }
12839
+ }
12840
+ diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
12404
12841
  await initSubscription(
12405
12842
  event.port,
12406
12843
  cfg,
@@ -12425,20 +12862,9 @@ function registerHooks(api, cfg) {
12425
12862
  }
12426
12863
  }
12427
12864
  logger.info(`cohort-sync: seeded telemetry for ${allAgentIds.length} agents`);
12428
- heartbeatInterval = setInterval(() => {
12429
- pushHeartbeat().catch((err) => {
12430
- logger.warn(`cohort-sync: heartbeat tick failed: ${String(err)}`);
12431
- });
12432
- }, 12e4);
12433
- logger.info("cohort-sync: heartbeat started (2m interval)");
12434
- activityFlushInterval = setInterval(() => {
12435
- flushActivityBuffer().catch((err) => {
12436
- logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
12437
- });
12438
- }, 1500);
12439
- logger.info("cohort-sync: activity flush started (1.5s interval)");
12440
12865
  });
12441
- api.on("agent_end", async (_event, ctx) => {
12866
+ api.on("agent_end", async (event, ctx) => {
12867
+ diag("HOOK_agent_end", { ctx: dumpCtx(ctx), success: event.success, error: event.error, durationMs: event.durationMs });
12442
12868
  const agentId = ctx.agentId ?? "main";
12443
12869
  const agentName = resolveAgentName(agentId);
12444
12870
  try {
@@ -12451,11 +12877,21 @@ function registerHooks(api, cfg) {
12451
12877
  tracker.markTelemetryPushed(agentName);
12452
12878
  }
12453
12879
  }
12880
+ if (event.success === false) {
12881
+ const entry = buildActivityEntry(agentName, "agent_end", {
12882
+ success: false,
12883
+ error: event.error,
12884
+ durationMs: event.durationMs,
12885
+ sessionKey: ctx.sessionKey
12886
+ });
12887
+ if (entry) addActivityToHot(entry);
12888
+ }
12454
12889
  } catch (err) {
12455
12890
  logger.warn(`cohort-sync: agent_end sync failed: ${String(err)}`);
12456
12891
  }
12457
12892
  });
12458
12893
  api.on("llm_output", async (event, ctx) => {
12894
+ diag("HOOK_llm_output", { ctx: dumpCtx(ctx), model: event.model, tokensIn: event.usage?.input, tokensOut: event.usage?.output });
12459
12895
  const agentId = ctx.agentId ?? "main";
12460
12896
  const agentName = resolveAgentName(agentId);
12461
12897
  try {
@@ -12469,6 +12905,13 @@ function registerHooks(api, cfg) {
12469
12905
  contextTokens: ext.contextTokens ?? usage.total ?? 0,
12470
12906
  contextLimit: ext.contextLimit ?? 0
12471
12907
  });
12908
+ if (sessionKey && !tracker.hasSession(agentName, sessionKey)) {
12909
+ tracker.addSession(agentName, sessionKey);
12910
+ logger.info(`cohort-sync: inferred session for ${agentName} from llm_output (${sessionKey})`);
12911
+ }
12912
+ if (sessionKey) {
12913
+ tracker.setSessionAgent(sessionKey, agentName);
12914
+ }
12472
12915
  if (tracker.shouldPushTelemetry(agentName)) {
12473
12916
  const snapshot = tracker.getTelemetrySnapshot(agentName);
12474
12917
  if (snapshot) {
@@ -12486,6 +12929,7 @@ function registerHooks(api, cfg) {
12486
12929
  }
12487
12930
  });
12488
12931
  api.on("after_compaction", async (event, ctx) => {
12932
+ diag("HOOK_after_compaction", { ctx: dumpCtx(ctx), messageCount: event.messageCount, tokenCount: event.tokenCount });
12489
12933
  const agentId = ctx.agentId ?? "main";
12490
12934
  const agentName = resolveAgentName(agentId);
12491
12935
  try {
@@ -12501,18 +12945,21 @@ function registerHooks(api, cfg) {
12501
12945
  tracker.markTelemetryPushed(agentName);
12502
12946
  }
12503
12947
  }
12504
- const entry = buildActivitySentence(agentName, "after_compaction", {
12505
- beforePercent: ext.beforePercent,
12506
- afterPercent: ext.afterPercent
12948
+ const entry = buildActivityEntry(agentName, "after_compaction", {
12949
+ messageCount: event.messageCount,
12950
+ compactedCount: ext.compactedCount ?? event.compactedCount,
12951
+ sessionKey: ctx.sessionKey
12507
12952
  });
12508
- if (entry) tracker.addActivity(entry);
12953
+ if (entry) addActivityToHot(entry);
12509
12954
  } catch (err) {
12510
12955
  logger.warn(`cohort-sync: after_compaction telemetry failed: ${String(err)}`);
12511
12956
  }
12512
12957
  });
12513
12958
  api.on("before_agent_start", async (_event, ctx) => {
12959
+ diag("HOOK_before_agent_start", { ctx: dumpCtx(ctx), event: dumpEvent(_event) });
12514
12960
  const agentId = ctx.agentId ?? "main";
12515
12961
  const agentName = resolveAgentName(agentId);
12962
+ diag("HOOK_before_agent_start_RESOLVED", { agentId, agentName, ctxChannelId: ctx.channelId, ctxMessageProvider: ctx.messageProvider, ctxSessionKey: ctx.sessionKey, ctxAccountId: ctx.accountId });
12516
12963
  try {
12517
12964
  tracker.updateStatus(agentName, "working");
12518
12965
  if (tracker.shouldPushTelemetry(agentName)) {
@@ -12522,19 +12969,37 @@ function registerHooks(api, cfg) {
12522
12969
  tracker.markTelemetryPushed(agentName);
12523
12970
  }
12524
12971
  }
12525
- const entry = buildActivitySentence(agentName, "before_agent_start", {
12526
- channel: ctx.messageProvider ?? "unknown"
12527
- });
12528
- if (entry) tracker.addActivity(entry);
12972
+ const sessionKey = ctx.sessionKey;
12973
+ if (sessionKey && !tracker.hasSession(agentName, sessionKey)) {
12974
+ tracker.addSession(agentName, sessionKey);
12975
+ tracker.setSessionAgent(sessionKey, agentName);
12976
+ logger.info(`cohort-sync: inferred session for ${agentName} (${sessionKey})`);
12977
+ if (tracker.shouldPushSessions(agentName)) {
12978
+ const sessSnapshot = tracker.getSessionsSnapshot(agentName);
12979
+ await pushSessions(cfg.apiKey, agentName, sessSnapshot);
12980
+ tracker.markSessionsPushed(agentName);
12981
+ }
12982
+ } else if (sessionKey) {
12983
+ tracker.setSessionAgent(sessionKey, agentName);
12984
+ }
12985
+ const ctxChannelId = ctx.channelId;
12986
+ if (ctxChannelId) {
12987
+ setChannelAgent(ctxChannelId, agentName);
12988
+ }
12989
+ const mp = ctx.messageProvider;
12990
+ if (mp) {
12991
+ setChannelAgent(mp, agentName);
12992
+ }
12529
12993
  } catch (err) {
12530
12994
  logger.warn(`cohort-sync: before_agent_start telemetry failed: ${String(err)}`);
12531
12995
  }
12532
12996
  });
12533
12997
  api.on("session_start", async (event, ctx) => {
12998
+ diag("HOOK_session_start", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
12534
12999
  const agentId = ctx.agentId ?? "main";
12535
13000
  const agentName = resolveAgentName(agentId);
12536
13001
  try {
12537
- const sessionKey = ctx.sessionKey ?? event.sessionId ?? String(Date.now());
13002
+ const sessionKey = ctx.sessionId ?? String(Date.now());
12538
13003
  tracker.addSession(agentName, sessionKey);
12539
13004
  if (tracker.shouldPushSessions(agentName)) {
12540
13005
  const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
@@ -12542,77 +13007,139 @@ function registerHooks(api, cfg) {
12542
13007
  tracker.markSessionsPushed(agentName);
12543
13008
  }
12544
13009
  const parsed = parseSessionKey(sessionKey);
12545
- const entry = buildActivitySentence(agentName, "session_start", {
12546
- channel: parsed.channel
13010
+ const entry = buildActivityEntry(agentName, "session_start", {
13011
+ channel: parsed.channel,
13012
+ sessionKey,
13013
+ resumedFrom: event.resumedFrom
12547
13014
  });
12548
- if (entry) tracker.addActivity(entry);
13015
+ if (entry) addActivityToHot(entry);
12549
13016
  } catch (err) {
12550
13017
  logger.warn(`cohort-sync: session_start tracking failed: ${String(err)}`);
12551
13018
  }
12552
13019
  });
12553
13020
  api.on("session_end", async (event, ctx) => {
13021
+ diag("HOOK_session_end", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
12554
13022
  const agentId = ctx.agentId ?? "main";
12555
13023
  const agentName = resolveAgentName(agentId);
12556
13024
  try {
12557
- const sessionKey = ctx.sessionKey ?? event.sessionId ?? "";
13025
+ const sessionKey = ctx.sessionId ?? "";
12558
13026
  tracker.removeSession(agentName, sessionKey);
12559
13027
  if (tracker.shouldPushSessions(agentName)) {
12560
13028
  const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
12561
13029
  await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
12562
13030
  tracker.markSessionsPushed(agentName);
12563
13031
  }
12564
- const entry = buildActivitySentence(agentName, "session_end", {});
12565
- if (entry) tracker.addActivity(entry);
13032
+ const entry = buildActivityEntry(agentName, "session_end", {
13033
+ sessionKey,
13034
+ messageCount: event.messageCount,
13035
+ durationMs: event.durationMs
13036
+ });
13037
+ if (entry) addActivityToHot(entry);
12566
13038
  } catch (err) {
12567
13039
  logger.warn(`cohort-sync: session_end tracking failed: ${String(err)}`);
12568
13040
  }
12569
13041
  });
12570
- api.on("before_tool_call", async (event, ctx) => {
12571
- const agentId = ctx.agentId ?? "main";
12572
- const agentName = resolveAgentName(agentId);
13042
+ api.on("after_tool_call", async (event, ctx) => {
13043
+ diag("HOOK_after_tool_call", { ctx: dumpCtx(ctx), toolName: event.toolName, error: event.error });
13044
+ const agentName = resolveAgentFromContext(ctx);
12573
13045
  try {
12574
- const entry = buildActivitySentence(agentName, "before_tool_call", {
12575
- toolName: event.toolName ?? event.name,
12576
- params: event.params ?? event.input
13046
+ const entry = buildActivityEntry(agentName, "after_tool_call", {
13047
+ toolName: event.toolName,
13048
+ params: event.params,
13049
+ error: event.error,
13050
+ durationMs: event.durationMs,
13051
+ sessionKey: ctx.sessionKey,
13052
+ model: resolveModel(ctx.agentId ?? "main")
12577
13053
  });
12578
- if (entry) tracker.addActivity(entry);
13054
+ if (entry) addActivityToHot(entry);
12579
13055
  } catch (err) {
12580
- logger.warn(`cohort-sync: before_tool_call activity failed: ${String(err)}`);
13056
+ logger.warn(`cohort-sync: after_tool_call activity failed: ${String(err)}`);
12581
13057
  }
12582
13058
  });
12583
13059
  api.on("message_received", async (_event, ctx) => {
13060
+ diag("HOOK_message_received_RAW", {
13061
+ ctx: dumpCtx(ctx),
13062
+ event: dumpEvent(_event),
13063
+ bridgeStateBefore: { ...getHotState().channelAgentBridge }
13064
+ });
13065
+ const agentName = resolveAgentFromContext(ctx);
13066
+ const channel = ctx.channelId;
13067
+ diag("HOOK_message_received_RESOLVED", {
13068
+ agentName,
13069
+ channel,
13070
+ accountId: ctx.accountId,
13071
+ conversationId: ctx.conversationId,
13072
+ from: _event.from
13073
+ });
13074
+ try {
13075
+ const entry = buildActivityEntry(agentName, "message_received", {
13076
+ channel: channel ?? "unknown"
13077
+ });
13078
+ if (entry) addActivityToHot(entry);
13079
+ } catch (err) {
13080
+ logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
13081
+ }
13082
+ });
13083
+ api.on("message_sent", async (event, ctx) => {
13084
+ diag("HOOK_message_sent_RAW", {
13085
+ ctx: dumpCtx(ctx),
13086
+ event: dumpEvent(event),
13087
+ bridgeStateBefore: { ...getHotState().channelAgentBridge }
13088
+ });
13089
+ const agentName = resolveAgentFromContext(ctx);
13090
+ const channel = ctx.channelId;
13091
+ diag("HOOK_message_sent_RESOLVED", {
13092
+ agentName,
13093
+ channel,
13094
+ accountId: ctx.accountId,
13095
+ conversationId: ctx.conversationId,
13096
+ to: event.to,
13097
+ success: event.success,
13098
+ error: event.error
13099
+ });
13100
+ try {
13101
+ const entry = buildActivityEntry(agentName, "message_sent", {
13102
+ channel: channel ?? "unknown",
13103
+ success: event.success,
13104
+ error: event.error
13105
+ });
13106
+ if (entry) addActivityToHot(entry);
13107
+ } catch (err) {
13108
+ logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
13109
+ }
13110
+ });
13111
+ api.on("before_compaction", async (_event, ctx) => {
13112
+ diag("HOOK_before_compaction", { ctx: dumpCtx(ctx) });
12584
13113
  const agentId = ctx.agentId ?? "main";
12585
13114
  const agentName = resolveAgentName(agentId);
12586
13115
  try {
12587
- const entry = buildActivitySentence(agentName, "message_received", {
12588
- channel: ctx.messageProvider ?? "unknown"
13116
+ const entry = buildActivityEntry(agentName, "before_compaction", {
13117
+ sessionKey: ctx.sessionKey
12589
13118
  });
12590
- if (entry) tracker.addActivity(entry);
13119
+ if (entry) addActivityToHot(entry);
12591
13120
  } catch (err) {
12592
- logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
13121
+ logger.warn(`cohort-sync: before_compaction activity failed: ${String(err)}`);
12593
13122
  }
12594
13123
  });
12595
- api.on("message_sent", async (_event, ctx) => {
13124
+ api.on("before_reset", async (event, ctx) => {
13125
+ diag("HOOK_before_reset", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
12596
13126
  const agentId = ctx.agentId ?? "main";
12597
13127
  const agentName = resolveAgentName(agentId);
12598
13128
  try {
12599
- const entry = buildActivitySentence(agentName, "message_sent", {
12600
- channel: ctx.messageProvider ?? "unknown"
13129
+ const entry = buildActivityEntry(agentName, "before_reset", {
13130
+ reason: event.reason,
13131
+ sessionKey: ctx.sessionKey
12601
13132
  });
12602
- if (entry) tracker.addActivity(entry);
13133
+ if (entry) addActivityToHot(entry);
12603
13134
  } catch (err) {
12604
- logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
13135
+ logger.warn(`cohort-sync: before_reset activity failed: ${String(err)}`);
12605
13136
  }
12606
13137
  });
12607
13138
  api.on("gateway_stop", async () => {
12608
- if (heartbeatInterval) {
12609
- clearInterval(heartbeatInterval);
12610
- heartbeatInterval = null;
12611
- }
12612
- if (activityFlushInterval) {
12613
- clearInterval(activityFlushInterval);
12614
- activityFlushInterval = null;
12615
- }
13139
+ diag("HOOK_gateway_stop", { bridgeState: { ...getHotState().channelAgentBridge } });
13140
+ clearIntervalsFromHot();
13141
+ heartbeatInterval = null;
13142
+ activityFlushInterval = null;
12616
13143
  await flushActivityBuffer().catch((err) => {
12617
13144
  logger.warn(`cohort-sync: final activity flush failed: ${String(err)}`);
12618
13145
  });
@@ -12629,7 +13156,7 @@ function registerHooks(api, cfg) {
12629
13156
  logger.warn(`cohort-sync: final unreachable push failed for ${agentName}: ${String(err)}`);
12630
13157
  }
12631
13158
  }
12632
- saveAgentStatesToHot(tracker.exportState());
13159
+ saveSessionsToDisk(tracker);
12633
13160
  try {
12634
13161
  await markAllUnreachable(cfg, logger);
12635
13162
  } catch (err) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Syncs agent status and skills to 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.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",