@cfio/cohort-sync 0.2.1 → 0.2.3

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,27 +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;
11746
- }
11755
+ function saveIntervalsToHot(heartbeat, activityFlush) {
11756
+ const state = getHotState();
11757
+ state.intervals = { heartbeat, activityFlush };
11747
11758
  }
11748
- function getAgentStatesFromHot() {
11749
- return getHotState()?.agentStates ?? null;
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 };
11750
11764
  }
11751
- function saveIntervalsToHot(heartbeat, activityFlush) {
11752
- const hot = getHotState();
11753
- if (hot) {
11754
- hot.intervals = { heartbeat, activityFlush };
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}"`);
11755
11771
  }
11756
11772
  }
11757
- function clearIntervalsFromHot() {
11758
- const hot = getHotState();
11759
- if (!hot?.intervals) return;
11760
- if (hot.intervals.heartbeat) clearInterval(hot.intervals.heartbeat);
11761
- if (hot.intervals.activityFlush) clearInterval(hot.intervals.activityFlush);
11762
- hot.intervals = { heartbeat: null, activityFlush: 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;
11763
11784
  }
11764
11785
 
11765
11786
  // src/sync.ts
@@ -11990,6 +12011,9 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
11990
12011
  logger.info("cohort-sync: full sync complete");
11991
12012
  }
11992
12013
 
12014
+ // src/agent-state.ts
12015
+ import { basename } from "node:path";
12016
+
11993
12017
  // src/session-key.ts
11994
12018
  function parseSessionKey(key) {
11995
12019
  if (!key || !key.includes(":")) {
@@ -12056,57 +12080,180 @@ function shouldPushSessions(current, lastPushed) {
12056
12080
  }
12057
12081
  return false;
12058
12082
  }
12059
- function basename(filePath) {
12060
- const parts = filePath.split("/");
12061
- 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);
12062
12087
  }
12063
- 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) {
12064
12095
  const ts = Date.now();
12065
12096
  const name = agentName.charAt(0).toUpperCase() + agentName.slice(1);
12066
- switch (event) {
12067
- 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": {
12068
12102
  const toolName = String(context.toolName ?? "a tool");
12069
12103
  const params = context.params;
12070
- if (toolName === "Edit" || toolName === "Write") {
12071
- const file = params?.file_path ? basename(String(params.file_path)) : "a file";
12072
- return { agentName, text: `${name} is editing ${file}`, category: "tool", timestamp: ts };
12073
- }
12104
+ const error = context.error;
12105
+ const durationMs = context.durationMs;
12106
+ const hasError = error != null && error !== "";
12107
+ let text;
12074
12108
  if (toolName === "Read") {
12075
12109
  const file = params?.file_path ? basename(String(params.file_path)) : "a file";
12076
- return { agentName, text: `${name} is reading ${file}`, category: "tool", timestamp: ts };
12077
- }
12078
- if (toolName === "Bash") {
12079
- return { agentName, text: `${name} is running a command`, category: "tool", timestamp: ts };
12080
- }
12081
- 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") {
12082
12117
  const taskNum = params?.task_number ?? "?";
12083
- 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}`;
12084
12121
  }
12085
- 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
+ };
12086
12135
  }
12087
12136
  case "message_received": {
12088
- const ch = String(context.channel ?? context.messageProvider ?? "unknown");
12089
- 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
+ };
12090
12148
  }
12091
12149
  case "message_sent": {
12092
- const ch = String(context.channel ?? context.messageProvider ?? "unknown");
12093
- return { agentName, text: `${name} sent a reply on ${ch}`, category: "message", timestamp: ts };
12094
- }
12095
- case "after_compaction": {
12096
- const before = context.beforePercent != null ? `${context.beforePercent}%` : "?";
12097
- const after = context.afterPercent != null ? `${context.afterPercent}%` : "?";
12098
- 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
+ };
12099
12166
  }
12100
12167
  case "session_start": {
12101
- const ch = String(context.channel ?? "unknown");
12102
- 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
+ };
12103
12181
  }
12104
12182
  case "session_end": {
12105
- 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
+ };
12106
12199
  }
12107
- case "before_agent_start": {
12108
- const ch = String(context.channel ?? "unknown");
12109
- return { agentName, text: `${name} is working on ${ch}`, category: "session", timestamp: ts };
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
+ };
12210
+ }
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
+ };
12110
12257
  }
12111
12258
  default:
12112
12259
  return null;
@@ -12183,6 +12330,9 @@ var AgentStateTracker = class {
12183
12330
  return pruned;
12184
12331
  }
12185
12332
  // --- Telemetry updates ---
12333
+ getStatus(agentName) {
12334
+ return this.agents.get(agentName)?.status ?? null;
12335
+ }
12186
12336
  updateStatus(agentName, status) {
12187
12337
  this.getOrCreate(agentName).status = status;
12188
12338
  }
@@ -12288,7 +12438,8 @@ var AgentStateTracker = class {
12288
12438
  exportState() {
12289
12439
  return {
12290
12440
  agents: new Map(this.agents),
12291
- sessionKeyToAgent: new Map(this.sessionKeyToAgent)
12441
+ sessionKeyToAgent: new Map(this.sessionKeyToAgent),
12442
+ activityBuffer: [...this.activityBuffer]
12292
12443
  };
12293
12444
  }
12294
12445
  importState(states) {
@@ -12299,6 +12450,9 @@ var AgentStateTracker = class {
12299
12450
  }
12300
12451
  this.agents.set(name, state);
12301
12452
  }
12453
+ if (!(states instanceof Map) && states.activityBuffer) {
12454
+ this.activityBuffer.push(...states.activityBuffer);
12455
+ }
12302
12456
  if (!(states instanceof Map) && states.sessionKeyToAgent) {
12303
12457
  for (const [key, agent] of states.sessionKeyToAgent) {
12304
12458
  this.sessionKeyToAgent.set(key, agent);
@@ -12308,6 +12462,43 @@ var AgentStateTracker = class {
12308
12462
  };
12309
12463
 
12310
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
+ }
12311
12502
  var PLUGIN_VERSION = "unknown";
12312
12503
  try {
12313
12504
  const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "package.json");
@@ -12315,6 +12506,7 @@ try {
12315
12506
  PLUGIN_VERSION = pkgJson.version ?? "unknown";
12316
12507
  } catch {
12317
12508
  }
12509
+ diag("MODULE_LOADED", { BUILD_ID, PLUGIN_VERSION });
12318
12510
  function parseIdentityFile(workspaceDir) {
12319
12511
  try {
12320
12512
  const filePath = path.join(workspaceDir, "IDENTITY.md");
@@ -12347,34 +12539,175 @@ function resolveIdentity(configIdentity, workspaceDir) {
12347
12539
  avatar: configIdentity?.avatar ?? fileIdentity?.avatar
12348
12540
  };
12349
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
+ }
12350
12600
  function registerHooks(api, cfg) {
12351
12601
  const { logger, config } = api;
12352
12602
  const nameMap = cfg.agentNameMap;
12353
- const tracker = new AgentStateTracker();
12603
+ const tracker = getOrCreateTracker();
12354
12604
  let heartbeatInterval = null;
12355
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
+ });
12356
12617
  setConvexUrl(cfg);
12357
12618
  setLogger(logger);
12358
12619
  restoreFromHotReload(logger);
12359
12620
  restoreRosterFromHotReload(getRosterHotState(), logger);
12360
- const savedStates = getAgentStatesFromHot();
12361
- if (savedStates) {
12362
- tracker.importState(savedStates);
12363
- 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
+ }
12364
12635
  }
12365
12636
  function resolveAgentName(agentId) {
12366
12637
  return (nameMap?.[agentId] ?? agentId).toLowerCase();
12367
12638
  }
12368
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
+ });
12369
12652
  if (ctx.agentId && typeof ctx.agentId === "string") {
12370
- return resolveAgentName(ctx.agentId);
12653
+ const resolved2 = resolveAgentName(ctx.agentId);
12654
+ diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "agentId", raw: ctx.agentId, resolved: resolved2 });
12655
+ return resolved2;
12371
12656
  }
12372
12657
  const sessionKey = ctx.sessionKey ?? ctx.sessionId;
12373
12658
  if (sessionKey && typeof sessionKey === "string") {
12374
12659
  const mapped = tracker.getSessionAgent(sessionKey);
12375
- if (mapped) return mapped;
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;
12376
12697
  }
12377
- return resolveAgentName("main");
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`);
12378
12711
  }
12379
12712
  clearIntervalsFromHot();
12380
12713
  heartbeatInterval = setInterval(() => {
@@ -12386,9 +12719,9 @@ function registerHooks(api, cfg) {
12386
12719
  flushActivityBuffer().catch((err) => {
12387
12720
  logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
12388
12721
  });
12389
- }, 1500);
12722
+ }, 3e3);
12390
12723
  saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
12391
- logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=1.5s)");
12724
+ logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=3s)");
12392
12725
  api.registerTool((toolCtx) => {
12393
12726
  const agentId = toolCtx.agentId ?? "main";
12394
12727
  const agentName = resolveAgentName(agentId);
@@ -12433,11 +12766,23 @@ function registerHooks(api, cfg) {
12433
12766
  if (m && typeof m === "object" && "primary" in m) return String(m.primary);
12434
12767
  return "unknown";
12435
12768
  }
12769
+ function getModelContextLimit(model) {
12770
+ const m = model.toLowerCase();
12771
+ if (m.includes("opus") || m.includes("sonnet") || m.includes("haiku")) return 2e5;
12772
+ if (m.includes("gpt-4o")) return 128e3;
12773
+ if (m.includes("gpt-4-turbo") || m.includes("gpt-4-1")) return 128e3;
12774
+ if (m.includes("gpt-4")) return 8192;
12775
+ if (m.includes("o3") || m.includes("o4-mini")) return 2e5;
12776
+ if (m.includes("gemini-2")) return 1e6;
12777
+ if (m.includes("gemini")) return 128e3;
12778
+ if (m.includes("deepseek")) return 128e3;
12779
+ return 2e5;
12780
+ }
12436
12781
  async function pushHeartbeat() {
12437
12782
  const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
12438
12783
  for (const agentId of allAgentIds) {
12439
12784
  const agentName = resolveAgentName(agentId);
12440
- const pruned = tracker.pruneStaleSessions(agentName, 6e5);
12785
+ const pruned = tracker.pruneStaleSessions(agentName, 864e5);
12441
12786
  if (pruned.length > 0) {
12442
12787
  logger.info(`cohort-sync: pruned ${pruned.length} stale sessions for ${agentName}`);
12443
12788
  }
@@ -12467,10 +12812,11 @@ function registerHooks(api, cfg) {
12467
12812
  logger.warn(`cohort-sync: heartbeat session push failed for ${agentName}: ${String(err)}`);
12468
12813
  }
12469
12814
  }
12815
+ saveSessionsToDisk(tracker);
12470
12816
  logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
12471
12817
  }
12472
12818
  async function flushActivityBuffer() {
12473
- const entries = tracker.flushActivity();
12819
+ const entries = drainActivityFromHot();
12474
12820
  if (entries.length === 0) return;
12475
12821
  try {
12476
12822
  await pushActivity(cfg.apiKey, entries);
@@ -12479,6 +12825,7 @@ function registerHooks(api, cfg) {
12479
12825
  }
12480
12826
  }
12481
12827
  api.on("gateway_start", async (event) => {
12828
+ diag("HOOK_gateway_start", { port: event.port, eventKeys: Object.keys(event) });
12482
12829
  try {
12483
12830
  checkForUpdate(PLUGIN_VERSION, logger).catch(() => {
12484
12831
  });
@@ -12494,6 +12841,15 @@ function registerHooks(api, cfg) {
12494
12841
  } catch (err) {
12495
12842
  logger.error(`cohort-sync: gateway_start sync failed: ${String(err)}`);
12496
12843
  }
12844
+ for (const a of config?.agents?.list ?? []) {
12845
+ const agentName = resolveAgentName(a.id);
12846
+ const mp = a.messageProvider;
12847
+ diag("GATEWAY_START_SEED_BRIDGE", { agentId: a.id, agentName, messageProvider: mp, accountId: a.accountId });
12848
+ if (mp && typeof mp === "string") {
12849
+ setChannelAgent(mp, agentName);
12850
+ }
12851
+ }
12852
+ diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
12497
12853
  await initSubscription(
12498
12854
  event.port,
12499
12855
  cfg,
@@ -12519,7 +12875,8 @@ function registerHooks(api, cfg) {
12519
12875
  }
12520
12876
  logger.info(`cohort-sync: seeded telemetry for ${allAgentIds.length} agents`);
12521
12877
  });
12522
- api.on("agent_end", async (_event, ctx) => {
12878
+ api.on("agent_end", async (event, ctx) => {
12879
+ diag("HOOK_agent_end", { ctx: dumpCtx(ctx), success: event.success, error: event.error, durationMs: event.durationMs });
12523
12880
  const agentId = ctx.agentId ?? "main";
12524
12881
  const agentName = resolveAgentName(agentId);
12525
12882
  try {
@@ -12532,23 +12889,44 @@ function registerHooks(api, cfg) {
12532
12889
  tracker.markTelemetryPushed(agentName);
12533
12890
  }
12534
12891
  }
12892
+ if (event.success === false) {
12893
+ const entry = buildActivityEntry(agentName, "agent_end", {
12894
+ success: false,
12895
+ error: event.error,
12896
+ durationMs: event.durationMs,
12897
+ sessionKey: ctx.sessionKey
12898
+ });
12899
+ if (entry) addActivityToHot(entry);
12900
+ }
12535
12901
  } catch (err) {
12536
12902
  logger.warn(`cohort-sync: agent_end sync failed: ${String(err)}`);
12537
12903
  }
12538
12904
  });
12539
12905
  api.on("llm_output", async (event, ctx) => {
12906
+ const usage = event.usage ?? {};
12907
+ const contextTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
12908
+ const model = event.model ?? resolveModel(ctx.agentId ?? "main");
12909
+ const contextLimit = getModelContextLimit(model);
12910
+ diag("HOOK_llm_output", {
12911
+ ctx: dumpCtx(ctx),
12912
+ model,
12913
+ tokensIn: usage.input,
12914
+ tokensOut: usage.output,
12915
+ cacheRead: usage.cacheRead,
12916
+ cacheWrite: usage.cacheWrite,
12917
+ contextTokens,
12918
+ contextLimit
12919
+ });
12540
12920
  const agentId = ctx.agentId ?? "main";
12541
12921
  const agentName = resolveAgentName(agentId);
12542
12922
  try {
12543
- const usage = event.usage ?? {};
12544
- const ext = event;
12545
12923
  const sessionKey = ctx.sessionKey;
12546
12924
  tracker.updateFromLlmOutput(agentName, sessionKey, {
12547
- model: event.model ?? resolveModel(agentId),
12925
+ model,
12548
12926
  tokensIn: usage.input ?? 0,
12549
12927
  tokensOut: usage.output ?? 0,
12550
- contextTokens: ext.contextTokens ?? usage.total ?? 0,
12551
- contextLimit: ext.contextLimit ?? 0
12928
+ contextTokens,
12929
+ contextLimit
12552
12930
  });
12553
12931
  if (sessionKey && !tracker.hasSession(agentName, sessionKey)) {
12554
12932
  tracker.addSession(agentName, sessionKey);
@@ -12574,13 +12952,13 @@ function registerHooks(api, cfg) {
12574
12952
  }
12575
12953
  });
12576
12954
  api.on("after_compaction", async (event, ctx) => {
12955
+ diag("HOOK_after_compaction", { ctx: dumpCtx(ctx), messageCount: event.messageCount, tokenCount: event.tokenCount });
12577
12956
  const agentId = ctx.agentId ?? "main";
12578
12957
  const agentName = resolveAgentName(agentId);
12579
12958
  try {
12580
- const ext = event;
12581
12959
  tracker.updateFromCompaction(agentName, {
12582
- contextTokens: ext.contextTokens ?? event.tokenCount ?? 0,
12583
- contextLimit: ext.contextLimit ?? 0
12960
+ contextTokens: event.tokenCount ?? 0,
12961
+ contextLimit: getModelContextLimit(resolveModel(agentId))
12584
12962
  });
12585
12963
  if (tracker.shouldPushTelemetry(agentName)) {
12586
12964
  const snapshot = tracker.getTelemetrySnapshot(agentName);
@@ -12589,18 +12967,21 @@ function registerHooks(api, cfg) {
12589
12967
  tracker.markTelemetryPushed(agentName);
12590
12968
  }
12591
12969
  }
12592
- const entry = buildActivitySentence(agentName, "after_compaction", {
12593
- beforePercent: ext.beforePercent,
12594
- afterPercent: ext.afterPercent
12970
+ const entry = buildActivityEntry(agentName, "after_compaction", {
12971
+ messageCount: event.messageCount,
12972
+ compactedCount: event.compactedCount,
12973
+ sessionKey: ctx.sessionKey
12595
12974
  });
12596
- if (entry) tracker.addActivity(entry);
12975
+ if (entry) addActivityToHot(entry);
12597
12976
  } catch (err) {
12598
12977
  logger.warn(`cohort-sync: after_compaction telemetry failed: ${String(err)}`);
12599
12978
  }
12600
12979
  });
12601
12980
  api.on("before_agent_start", async (_event, ctx) => {
12981
+ diag("HOOK_before_agent_start", { ctx: dumpCtx(ctx), event: dumpEvent(_event) });
12602
12982
  const agentId = ctx.agentId ?? "main";
12603
12983
  const agentName = resolveAgentName(agentId);
12984
+ diag("HOOK_before_agent_start_RESOLVED", { agentId, agentName, ctxChannelId: ctx.channelId, ctxMessageProvider: ctx.messageProvider, ctxSessionKey: ctx.sessionKey, ctxAccountId: ctx.accountId });
12604
12985
  try {
12605
12986
  tracker.updateStatus(agentName, "working");
12606
12987
  if (tracker.shouldPushTelemetry(agentName)) {
@@ -12620,20 +13001,27 @@ function registerHooks(api, cfg) {
12620
13001
  await pushSessions(cfg.apiKey, agentName, sessSnapshot);
12621
13002
  tracker.markSessionsPushed(agentName);
12622
13003
  }
13004
+ } else if (sessionKey) {
13005
+ tracker.setSessionAgent(sessionKey, agentName);
13006
+ }
13007
+ const ctxChannelId = ctx.channelId;
13008
+ if (ctxChannelId) {
13009
+ setChannelAgent(ctxChannelId, agentName);
13010
+ }
13011
+ const mp = ctx.messageProvider;
13012
+ if (mp) {
13013
+ setChannelAgent(mp, agentName);
12623
13014
  }
12624
- const entry = buildActivitySentence(agentName, "before_agent_start", {
12625
- channel: ctx.messageProvider ?? "unknown"
12626
- });
12627
- if (entry) tracker.addActivity(entry);
12628
13015
  } catch (err) {
12629
13016
  logger.warn(`cohort-sync: before_agent_start telemetry failed: ${String(err)}`);
12630
13017
  }
12631
13018
  });
12632
13019
  api.on("session_start", async (event, ctx) => {
13020
+ diag("HOOK_session_start", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
12633
13021
  const agentId = ctx.agentId ?? "main";
12634
13022
  const agentName = resolveAgentName(agentId);
12635
13023
  try {
12636
- const sessionKey = ctx.sessionKey ?? event.sessionId ?? String(Date.now());
13024
+ const sessionKey = ctx.sessionId ?? String(Date.now());
12637
13025
  tracker.addSession(agentName, sessionKey);
12638
13026
  if (tracker.shouldPushSessions(agentName)) {
12639
13027
  const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
@@ -12641,66 +13029,136 @@ function registerHooks(api, cfg) {
12641
13029
  tracker.markSessionsPushed(agentName);
12642
13030
  }
12643
13031
  const parsed = parseSessionKey(sessionKey);
12644
- const entry = buildActivitySentence(agentName, "session_start", {
12645
- channel: parsed.channel
13032
+ const entry = buildActivityEntry(agentName, "session_start", {
13033
+ channel: parsed.channel,
13034
+ sessionKey,
13035
+ resumedFrom: event.resumedFrom
12646
13036
  });
12647
- if (entry) tracker.addActivity(entry);
13037
+ if (entry) addActivityToHot(entry);
12648
13038
  } catch (err) {
12649
13039
  logger.warn(`cohort-sync: session_start tracking failed: ${String(err)}`);
12650
13040
  }
12651
13041
  });
12652
13042
  api.on("session_end", async (event, ctx) => {
13043
+ diag("HOOK_session_end", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
12653
13044
  const agentId = ctx.agentId ?? "main";
12654
13045
  const agentName = resolveAgentName(agentId);
12655
13046
  try {
12656
- const sessionKey = ctx.sessionKey ?? event.sessionId ?? "";
13047
+ const sessionKey = ctx.sessionId ?? "";
12657
13048
  tracker.removeSession(agentName, sessionKey);
12658
13049
  if (tracker.shouldPushSessions(agentName)) {
12659
13050
  const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
12660
13051
  await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
12661
13052
  tracker.markSessionsPushed(agentName);
12662
13053
  }
12663
- const entry = buildActivitySentence(agentName, "session_end", {});
12664
- if (entry) tracker.addActivity(entry);
13054
+ const entry = buildActivityEntry(agentName, "session_end", {
13055
+ sessionKey,
13056
+ messageCount: event.messageCount,
13057
+ durationMs: event.durationMs
13058
+ });
13059
+ if (entry) addActivityToHot(entry);
12665
13060
  } catch (err) {
12666
13061
  logger.warn(`cohort-sync: session_end tracking failed: ${String(err)}`);
12667
13062
  }
12668
13063
  });
12669
- api.on("before_tool_call", async (event, ctx) => {
13064
+ api.on("after_tool_call", async (event, ctx) => {
13065
+ diag("HOOK_after_tool_call", { ctx: dumpCtx(ctx), toolName: event.toolName, error: event.error });
12670
13066
  const agentName = resolveAgentFromContext(ctx);
12671
13067
  try {
12672
- const entry = buildActivitySentence(agentName, "before_tool_call", {
12673
- toolName: event.toolName ?? event.name,
12674
- params: event.params ?? event.input
13068
+ const entry = buildActivityEntry(agentName, "after_tool_call", {
13069
+ toolName: event.toolName,
13070
+ params: event.params,
13071
+ error: event.error,
13072
+ durationMs: event.durationMs,
13073
+ sessionKey: ctx.sessionKey,
13074
+ model: resolveModel(ctx.agentId ?? "main")
12675
13075
  });
12676
- if (entry) tracker.addActivity(entry);
13076
+ if (entry) addActivityToHot(entry);
12677
13077
  } catch (err) {
12678
- logger.warn(`cohort-sync: before_tool_call activity failed: ${String(err)}`);
13078
+ logger.warn(`cohort-sync: after_tool_call activity failed: ${String(err)}`);
12679
13079
  }
12680
13080
  });
12681
13081
  api.on("message_received", async (_event, ctx) => {
13082
+ diag("HOOK_message_received_RAW", {
13083
+ ctx: dumpCtx(ctx),
13084
+ event: dumpEvent(_event),
13085
+ bridgeStateBefore: { ...getHotState().channelAgentBridge }
13086
+ });
12682
13087
  const agentName = resolveAgentFromContext(ctx);
13088
+ const channel = ctx.channelId;
13089
+ diag("HOOK_message_received_RESOLVED", {
13090
+ agentName,
13091
+ channel,
13092
+ accountId: ctx.accountId,
13093
+ conversationId: ctx.conversationId,
13094
+ from: _event.from
13095
+ });
12683
13096
  try {
12684
- const entry = buildActivitySentence(agentName, "message_received", {
12685
- channel: ctx.messageProvider ?? "unknown"
13097
+ const entry = buildActivityEntry(agentName, "message_received", {
13098
+ channel: channel ?? "unknown"
12686
13099
  });
12687
- if (entry) tracker.addActivity(entry);
13100
+ if (entry) addActivityToHot(entry);
12688
13101
  } catch (err) {
12689
13102
  logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
12690
13103
  }
12691
13104
  });
12692
- api.on("message_sent", async (_event, ctx) => {
13105
+ api.on("message_sent", async (event, ctx) => {
13106
+ diag("HOOK_message_sent_RAW", {
13107
+ ctx: dumpCtx(ctx),
13108
+ event: dumpEvent(event),
13109
+ bridgeStateBefore: { ...getHotState().channelAgentBridge }
13110
+ });
12693
13111
  const agentName = resolveAgentFromContext(ctx);
13112
+ const channel = ctx.channelId;
13113
+ diag("HOOK_message_sent_RESOLVED", {
13114
+ agentName,
13115
+ channel,
13116
+ accountId: ctx.accountId,
13117
+ conversationId: ctx.conversationId,
13118
+ to: event.to,
13119
+ success: event.success,
13120
+ error: event.error
13121
+ });
12694
13122
  try {
12695
- const entry = buildActivitySentence(agentName, "message_sent", {
12696
- channel: ctx.messageProvider ?? "unknown"
13123
+ const entry = buildActivityEntry(agentName, "message_sent", {
13124
+ channel: channel ?? "unknown",
13125
+ success: event.success,
13126
+ error: event.error
12697
13127
  });
12698
- if (entry) tracker.addActivity(entry);
13128
+ if (entry) addActivityToHot(entry);
12699
13129
  } catch (err) {
12700
13130
  logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
12701
13131
  }
12702
13132
  });
13133
+ api.on("before_compaction", async (_event, ctx) => {
13134
+ diag("HOOK_before_compaction", { ctx: dumpCtx(ctx) });
13135
+ const agentId = ctx.agentId ?? "main";
13136
+ const agentName = resolveAgentName(agentId);
13137
+ try {
13138
+ const entry = buildActivityEntry(agentName, "before_compaction", {
13139
+ sessionKey: ctx.sessionKey
13140
+ });
13141
+ if (entry) addActivityToHot(entry);
13142
+ } catch (err) {
13143
+ logger.warn(`cohort-sync: before_compaction activity failed: ${String(err)}`);
13144
+ }
13145
+ });
13146
+ api.on("before_reset", async (event, ctx) => {
13147
+ diag("HOOK_before_reset", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13148
+ const agentId = ctx.agentId ?? "main";
13149
+ const agentName = resolveAgentName(agentId);
13150
+ try {
13151
+ const entry = buildActivityEntry(agentName, "before_reset", {
13152
+ reason: event.reason,
13153
+ sessionKey: ctx.sessionKey
13154
+ });
13155
+ if (entry) addActivityToHot(entry);
13156
+ } catch (err) {
13157
+ logger.warn(`cohort-sync: before_reset activity failed: ${String(err)}`);
13158
+ }
13159
+ });
12703
13160
  api.on("gateway_stop", async () => {
13161
+ diag("HOOK_gateway_stop", { bridgeState: { ...getHotState().channelAgentBridge } });
12704
13162
  clearIntervalsFromHot();
12705
13163
  heartbeatInterval = null;
12706
13164
  activityFlushInterval = null;
@@ -12720,7 +13178,7 @@ function registerHooks(api, cfg) {
12720
13178
  logger.warn(`cohort-sync: final unreachable push failed for ${agentName}: ${String(err)}`);
12721
13179
  }
12722
13180
  }
12723
- saveAgentStatesToHot(tracker.exportState());
13181
+ saveSessionsToDisk(tracker);
12724
13182
  try {
12725
13183
  await markAllUnreachable(cfg, logger);
12726
13184
  } catch (err) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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.1",
3
+ "version": "0.2.3",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",