@cfio/cohort-sync 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13271,53 +13271,6 @@ var AgentStateTracker = class {
13271
13271
  }
13272
13272
  };
13273
13273
 
13274
- // src/pocket-guide.ts
13275
- var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
13276
-
13277
- > These are the essential rules. For the full guide, see https://docs.cohort.bot/integration/guide
13278
-
13279
- ## Getting Started
13280
- - Call the cohort_context tool at the start of every work session for your personalized briefing.
13281
- - If cohort_context is unavailable, call GET /api/v1/context directly.
13282
- - Do not skip the context call \u2014 it contains workspace-specific overrides that supersede these rules.
13283
-
13284
- ## Task Lifecycle
13285
- - Create tasks for trackable work items. Always set priority (default p2) and effort when estimable.
13286
- - Use POST /tasks/:id/transition for status changes. Never PATCH status directly \u2014 the server rejects it.
13287
- - If a transition is rejected, read the error response for valid transitions from the current status.
13288
- - You cannot transition tasks to "done" unless your workspace admin has enabled it. Only humans close tasks by default.
13289
- - When moving to "in_progress", you are claiming ownership \u2014 only claim tasks you can actively work on.
13290
- - When moving to "waiting", comment what you are blocked on and who/what can unblock you.
13291
- - When work is complete, comment with evidence: PR link, test output, or summary of changes.
13292
-
13293
- ## Comments
13294
- - Comment before every status transition explaining what happened.
13295
- - Post progress updates every 15-30 minutes on long-running work.
13296
- - Keep comments factual \u2014 what you did, what you found, what is next.
13297
- - No conversational filler. Every comment must contain information.
13298
- - Reference specific files, line numbers, error messages, or URLs when relevant.
13299
-
13300
- ## Projects & Initiatives
13301
- - Don't create projects or initiatives without explicit instruction.
13302
- - Assign tasks to existing projects when one fits. Don't create a project for a single task.
13303
- - If no project fits, leave the task unassigned to a project.
13304
-
13305
- ## Error Recovery
13306
- - If auth fails (401), stop immediately. Do not retry. Report the failure.
13307
- - If rate limited (429), wait per Retry-After header.
13308
- - If 404, verify you are using the correct task number or ID (both accepted).
13309
- - If 500, retry once after a brief pause. If it fails again, stop and report.
13310
-
13311
- ## What Not To Do
13312
- - Don't poll /tasks in a loop \u2014 check assignments, then do your work.
13313
- - Don't create duplicate tasks \u2014 search first.
13314
- - Don't delete tasks unless explicitly told to.
13315
- - Don't bulk-create tasks speculatively \u2014 create them as work becomes concrete.
13316
- - Don't modify tasks assigned to other agents unless coordinating through comments.
13317
- - Don't set "done" on tasks you didn't work on.
13318
- - Don't ignore workspace-specific overrides from your context response.
13319
- `;
13320
-
13321
13274
  // src/types.ts
13322
13275
  var DEFAULT_API_URL = "https://api.cohort.bot";
13323
13276
 
@@ -13381,13 +13334,7 @@ function dumpCtx(ctx) {
13381
13334
  function dumpEvent(event) {
13382
13335
  return dumpCtx(event);
13383
13336
  }
13384
- var PLUGIN_VERSION = true ? "0.10.3" : "unknown";
13385
- var _gatewayStartHandler = null;
13386
- async function handleGatewayStart(event) {
13387
- if (_gatewayStartHandler) {
13388
- await _gatewayStartHandler(event);
13389
- }
13390
- }
13337
+ var PLUGIN_VERSION = true ? "0.11.0" : "unknown";
13391
13338
  function resolveGatewayToken(api) {
13392
13339
  const token = api.config?.gateway?.auth?.token;
13393
13340
  return typeof token === "string" ? token : null;
@@ -13443,21 +13390,9 @@ function resolveIdentity(configIdentity, workspaceDir) {
13443
13390
  avatar: configIdentity?.avatar ?? fileIdentity?.avatar
13444
13391
  };
13445
13392
  }
13446
- var tracker = null;
13447
- function getOrCreateTracker() {
13448
- if (!tracker) tracker = new AgentStateTracker();
13449
- return tracker;
13450
- }
13451
- var gatewayPort = null;
13452
- var gatewayToken = null;
13453
- var persistentGwClient = null;
13454
- var gwClientInitialized = false;
13455
- var keepaliveInterval = null;
13456
- var commandUnsubscriber2 = null;
13457
- var STATE_FILE_PATH = "";
13458
- function saveSessionsToDisk(tracker2) {
13393
+ function saveSessionsToDisk(tracker, stateFilePath) {
13459
13394
  try {
13460
- const state = tracker2.exportState();
13395
+ const state = tracker.exportState();
13461
13396
  const data = {
13462
13397
  sessions: [],
13463
13398
  sessionKeyToAgent: Object.fromEntries(state.sessionKeyToAgent),
@@ -13469,27 +13404,27 @@ function saveSessionsToDisk(tracker2) {
13469
13404
  data.sessions.push({ agentName: name, key });
13470
13405
  }
13471
13406
  }
13472
- fs2.writeFileSync(STATE_FILE_PATH, JSON.stringify(data), { mode: 384 });
13407
+ fs2.writeFileSync(stateFilePath, JSON.stringify(data), { mode: 384 });
13473
13408
  } catch {
13474
13409
  }
13475
13410
  }
13476
- function loadSessionsFromDisk(tracker2, logger) {
13411
+ function loadSessionsFromDisk(tracker, stateFilePath, logger) {
13477
13412
  try {
13478
- if (!fs2.existsSync(STATE_FILE_PATH)) return;
13479
- const data = JSON.parse(fs2["read"+"FileSync"](STATE_FILE_PATH, "utf8"));
13413
+ if (!fs2.existsSync(stateFilePath)) return;
13414
+ const data = JSON.parse(fs2["read"+"FileSync"](stateFilePath, "utf8"));
13480
13415
  if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
13481
13416
  logger.info("cohort-sync: disk session state too old (>24h), skipping");
13482
13417
  return;
13483
13418
  }
13484
13419
  let count = 0;
13485
13420
  for (const { agentName, key } of data.sessions) {
13486
- if (!tracker2.hasSession(agentName, key)) {
13487
- tracker2.addSession(agentName, key);
13421
+ if (!tracker.hasSession(agentName, key)) {
13422
+ tracker.addSession(agentName, key);
13488
13423
  count++;
13489
13424
  }
13490
13425
  }
13491
13426
  for (const [key, agent] of Object.entries(data.sessionKeyToAgent)) {
13492
- tracker2.setSessionAgent(key, agent);
13427
+ tracker.setSessionAgent(key, agent);
13493
13428
  }
13494
13429
  for (const [channelId, agent] of Object.entries(data.channelAgents ?? {})) {
13495
13430
  setChannelAgent(channelId, agent);
@@ -13502,8 +13437,6 @@ function loadSessionsFromDisk(tracker2, logger) {
13502
13437
  }
13503
13438
  function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13504
13439
  const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
13505
- persistentGwClient = client2;
13506
- gwClientInitialized = true;
13507
13440
  const onConnected = async () => {
13508
13441
  logger.debug(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
13509
13442
  logger.info(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
@@ -13528,75 +13461,106 @@ function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
13528
13461
  logger.debug("cohort-sync: initial connect deferred", { error: String(err) });
13529
13462
  logger.warn(`cohort-sync: GW connect will retry: ${String(err)}`);
13530
13463
  });
13464
+ return client2;
13531
13465
  }
13532
- function registerHooks(api, cfg) {
13533
- STATE_FILE_PATH = path2.join(cfg.stateDir, "session-state.json");
13534
- const { logger, config } = api;
13535
- const nameMap = cfg.agentNameMap;
13536
- const tracker2 = getOrCreateTracker();
13537
- const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
13538
- createClient(convexUrl);
13539
- setLogger(logger);
13540
- gatewayPort = api.config?.gateway?.port ?? null;
13541
- gatewayToken = resolveGatewayToken(api);
13542
- if (gatewayPort && gatewayToken) {
13543
- initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
13544
- }
13545
- const cronStorePath = api.config?.cron?.store ?? path2.join(os2.homedir(), ".openclaw", "cron", "jobs.json");
13546
- for (const eventType of ["agent", "session", "command", "gateway", "message"]) {
13547
- api.registerHook(eventType, async (event) => {
13548
- logger.info(`cohort-sync: [DIAG] event=${eventType} action=${event?.action} sessionKey=${event?.sessionKey} ctxKeys=${Object.keys(event?.context ?? {}).join(",")}`);
13549
- }, { name: `cohort-sync.diag-${eventType}`, description: `Diagnostic: catch all ${eventType} events` });
13466
+ async function handleGatewayStart(event, state) {
13467
+ if (!state) return;
13468
+ const { cfg, tracker, logger, config, api } = state;
13469
+ logger.debug("cohort-sync: hook: gateway_start", { port: event.port, eventKeys: Object.keys(event) });
13470
+ try {
13471
+ checkForUpdate(PLUGIN_VERSION, logger).catch(() => {
13472
+ });
13473
+ } catch {
13550
13474
  }
13551
- logger.info(`cohort-sync: registerHooks v${PLUGIN_VERSION}`);
13552
- logger.info("cohort-sync: hooks registered", {
13553
- PLUGIN_VERSION,
13554
- hasNameMap: !!nameMap,
13555
- nameMapKeys: nameMap ? Object.keys(nameMap) : [],
13556
- nameMapValues: nameMap ? Object.values(nameMap) : [],
13557
- agentCount: config?.agents?.list?.length ?? 0,
13558
- agentIds: (config?.agents?.list ?? []).map((a) => a.id),
13559
- agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
13475
+ try {
13476
+ const agentList = (config?.agents?.list ?? []).map((a) => ({
13477
+ id: a.id,
13478
+ model: state.resolveModel(a.id),
13479
+ identity: resolveIdentity(a.identity, a.workspace)
13480
+ }));
13481
+ await fullSync(state.resolveAgentName("main"), state.resolveModel("main"), cfg, logger, agentList);
13482
+ } catch (err) {
13483
+ logger.error(`cohort-sync: gateway_start sync failed: ${String(err)}`);
13484
+ }
13485
+ for (const a of config?.agents?.list ?? []) {
13486
+ const agentName = state.resolveAgentName(a.id);
13487
+ const mp = a.messageProvider;
13488
+ logger.debug("cohort-sync: gateway start: seed bridge", { agentId: a.id, agentName, messageProvider: mp, accountId: a.accountId });
13489
+ if (mp && typeof mp === "string") {
13490
+ setChannelAgent(mp, agentName);
13491
+ }
13492
+ }
13493
+ logger.debug("cohort-sync: gateway start: bridge after seed", { bridge: Object.fromEntries(getChannelAgentBridge()) });
13494
+ state.gatewayPort = event.port;
13495
+ const token = resolveGatewayToken(api);
13496
+ if (token) {
13497
+ state.gatewayToken = token;
13498
+ logger.debug("cohort-sync: gateway client connecting", { port: event.port, hasToken: true });
13499
+ try {
13500
+ const client2 = initGatewayClient(event.port, token, cfg, state.resolveAgentName, logger);
13501
+ state.persistentGwClient = client2;
13502
+ state.gwClientInitialized = true;
13503
+ if (state.commandUnsubscriber) {
13504
+ state.commandUnsubscriber();
13505
+ state.commandUnsubscriber = null;
13506
+ }
13507
+ const unsub = startCommandSubscription(cfg, logger, state.resolveAgentName, state.persistentGwClient);
13508
+ state.commandUnsubscriber = unsub;
13509
+ } catch (err) {
13510
+ logger.debug("cohort-sync: gateway client connect failed", { error: String(err) });
13511
+ logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
13512
+ }
13513
+ } else {
13514
+ logger.debug("cohort-sync: no gateway token");
13515
+ logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
13516
+ }
13517
+ await startNotificationSubscription(
13518
+ event.port,
13519
+ cfg,
13520
+ api.config.hooks?.token,
13521
+ logger,
13522
+ state.persistentGwClient
13523
+ ).catch((err) => {
13524
+ logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
13560
13525
  });
13561
- const identityNameMap = {};
13562
- const mainIdentity = parseIdentityFile(process.cwd());
13563
- if (mainIdentity?.name) {
13564
- identityNameMap["main"] = mainIdentity.name.toLowerCase();
13565
- }
13566
- for (const agent of config?.agents?.list ?? []) {
13567
- const identity = resolveIdentity(agent.identity, agent.workspace);
13568
- if (identity?.name) {
13569
- identityNameMap[agent.id] = identity.name.toLowerCase();
13526
+ const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
13527
+ for (const agentId of allAgentIds) {
13528
+ const agentName = state.resolveAgentName(agentId);
13529
+ try {
13530
+ tracker.setModel(agentName, state.resolveModel(agentId));
13531
+ tracker.updateStatus(agentName, "idle");
13532
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
13533
+ if (snapshot) {
13534
+ await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
13535
+ tracker.markTelemetryPushed(agentName);
13536
+ }
13537
+ } catch (err) {
13538
+ logger.warn(`cohort-sync: initial telemetry seed failed for ${agentName}: ${String(err)}`);
13570
13539
  }
13571
13540
  }
13572
- logger.debug("cohort-sync: identity name map", { identityNameMap });
13573
- if (tracker2.getAgentNames().length === 0) {
13574
- loadSessionsFromDisk(tracker2, logger);
13575
- const restoredAgents = tracker2.getAgentNames();
13576
- for (const agentName of restoredAgents) {
13577
- const sessSnapshot = tracker2.getSessionsSnapshot(agentName);
13578
- if (sessSnapshot.length > 0) {
13579
- pushSessions(cfg.apiKey, agentName, sessSnapshot).then(() => {
13580
- tracker2.markSessionsPushed(agentName);
13581
- logger.info(`cohort-sync: pushed ${sessSnapshot.length} restored sessions for ${agentName}`);
13582
- }).catch((err) => {
13583
- logger.warn(`cohort-sync: failed to push restored sessions for ${agentName}: ${String(err)}`);
13541
+ logger.info(`cohort-sync: seeded telemetry for ${allAgentIds.length} agents`);
13542
+ if (state.keepaliveInterval) clearInterval(state.keepaliveInterval);
13543
+ state.keepaliveInterval = setInterval(async () => {
13544
+ for (const agentId of allAgentIds) {
13545
+ const agentName = state.resolveAgentName(agentId);
13546
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
13547
+ if (snapshot) {
13548
+ await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION }).catch(() => {
13584
13549
  });
13585
13550
  }
13586
13551
  }
13587
- }
13588
- function resolveAgentName(agentId) {
13589
- return (nameMap?.[agentId] ?? identityNameMap[agentId] ?? agentId).toLowerCase();
13590
- }
13591
- setToolRuntime({
13592
- apiKey: cfg.apiKey,
13593
- apiUrl: cfg.apiUrl,
13594
- resolveAgentName,
13595
- logger
13596
- });
13597
- function resolveAgentFromContext(ctx) {
13552
+ for (const agentId of allAgentIds) {
13553
+ const agentName = state.resolveAgentName(agentId);
13554
+ tracker.pruneStaleSessions(agentName, 864e5);
13555
+ }
13556
+ saveSessionsToDisk(tracker, state.stateFilePath);
13557
+ }, 15e4);
13558
+ logger.info("cohort-sync: keepalive interval started (150s)");
13559
+ }
13560
+ function registerHookHandlers(api, logger, getState) {
13561
+ function resolveAgentFromContext(state, ctx) {
13598
13562
  const allCtxKeys = Object.keys(ctx);
13599
- logger.debug("cohort-sync: resolve agent: start", {
13563
+ state.logger.debug("cohort-sync: resolve agent: start", {
13600
13564
  ctxKeys: allCtxKeys,
13601
13565
  agentId: ctx.agentId,
13602
13566
  sessionKey: ctx.sessionKey,
@@ -13608,208 +13572,73 @@ function registerHooks(api, cfg) {
13608
13572
  workspaceDir: ctx.workspaceDir
13609
13573
  });
13610
13574
  if (ctx.agentId && typeof ctx.agentId === "string") {
13611
- const resolved2 = resolveAgentName(ctx.agentId);
13612
- logger.debug("cohort-sync: resolve agent: result", { method: "agentId", raw: ctx.agentId, resolved: resolved2 });
13575
+ const resolved2 = state.resolveAgentName(ctx.agentId);
13576
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "agentId", raw: ctx.agentId, resolved: resolved2 });
13613
13577
  return resolved2;
13614
13578
  }
13615
13579
  const sessionKey = ctx.sessionKey ?? ctx.sessionId;
13616
13580
  if (sessionKey && typeof sessionKey === "string") {
13617
- const mapped = tracker2.getSessionAgent(sessionKey);
13581
+ const mapped = state.tracker.getSessionAgent(sessionKey);
13618
13582
  if (mapped) {
13619
- logger.debug("cohort-sync: resolve agent: result", { method: "sessionKey_mapped", sessionKey, mapped });
13583
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "sessionKey_mapped", sessionKey, mapped });
13620
13584
  return mapped;
13621
13585
  }
13622
13586
  const parts = sessionKey.split(":");
13623
13587
  if (parts[0] === "agent" && parts.length >= 2) {
13624
- const resolved2 = resolveAgentName(parts[1]);
13625
- logger.debug("cohort-sync: resolve agent: result", { method: "sessionKey_parsed", sessionKey, agentPart: parts[1], resolved: resolved2 });
13588
+ const resolved2 = state.resolveAgentName(parts[1]);
13589
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "sessionKey_parsed", sessionKey, agentPart: parts[1], resolved: resolved2 });
13626
13590
  return resolved2;
13627
13591
  }
13628
13592
  }
13629
13593
  const accountId = ctx.accountId;
13630
13594
  if (accountId && typeof accountId === "string") {
13631
- const resolved2 = resolveAgentName(accountId);
13632
- logger.debug("cohort-sync: resolve agent: result", { method: "accountId", accountId, resolved: resolved2 });
13595
+ const resolved2 = state.resolveAgentName(accountId);
13596
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "accountId", accountId, resolved: resolved2 });
13633
13597
  return resolved2;
13634
13598
  }
13635
13599
  const channelId = ctx.channelId;
13636
13600
  if (channelId && typeof channelId === "string") {
13637
13601
  const channelAgent = getChannelAgent(channelId);
13638
13602
  if (channelAgent) {
13639
- logger.debug("cohort-sync: resolve agent: result", { method: "channelId_bridge", channelId, channelAgent, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
13603
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "channelId_bridge", channelId, channelAgent, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
13640
13604
  return channelAgent;
13641
13605
  }
13642
- logger.debug("cohort-sync: resolve agent: result", { method: "channelId_raw", channelId, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
13606
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "channelId_raw", channelId, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
13643
13607
  return String(channelId);
13644
13608
  }
13645
- const resolved = resolveAgentName("main");
13646
- logger.debug("cohort-sync: resolve agent: result", { method: "fallback_main", resolved });
13609
+ const resolved = state.resolveAgentName("main");
13610
+ state.logger.debug("cohort-sync: resolve agent: result", { method: "fallback_main", resolved });
13647
13611
  return resolved;
13648
13612
  }
13649
- function resolveChannelFromContext(ctx) {
13650
- if (ctx.messageProvider && typeof ctx.messageProvider === "string") {
13651
- return ctx.messageProvider;
13652
- }
13653
- if (ctx.channelId && typeof ctx.channelId === "string") {
13654
- return ctx.channelId;
13655
- }
13656
- const sessionKey = ctx.sessionKey ?? ctx.sessionId;
13657
- if (sessionKey && typeof sessionKey === "string") {
13658
- const parsed = parseSessionKey(sessionKey);
13659
- return parsed.channel;
13660
- }
13661
- return "unknown";
13662
- }
13663
- const activityBatch = new MicroBatch({
13664
- maxSize: 10,
13665
- maxDelayMs: 1e3,
13666
- flush: (entries) => pushActivity(cfg.apiKey, entries),
13667
- onError: (err) => logger.warn(`cohort-sync: activity batch flush failed: ${String(err)}`)
13668
- });
13669
- const KEEPALIVE_INTERVAL_MS = 15e4;
13670
- {
13671
- if (commandUnsubscriber2) {
13672
- commandUnsubscriber2();
13673
- commandUnsubscriber2 = null;
13674
- }
13675
- const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
13676
- commandUnsubscriber2 = unsub;
13677
- }
13678
- function resolveModel(agentId) {
13679
- const agent = config?.agents?.list?.find((a) => a.id === agentId);
13680
- const m = agent?.model;
13681
- if (typeof m === "string") return m;
13682
- if (m && typeof m === "object" && "primary" in m) return String(m.primary);
13683
- return "unknown";
13684
- }
13685
- function getModelContextLimit(model) {
13686
- const m = model.toLowerCase();
13687
- if (m.includes("opus") || m.includes("sonnet") || m.includes("haiku")) return 2e5;
13688
- if (m.includes("gpt-4o")) return 128e3;
13689
- if (m.includes("gpt-4-turbo") || m.includes("gpt-4-1")) return 128e3;
13690
- if (m.includes("gpt-4")) return 8192;
13691
- if (m.includes("o3") || m.includes("o4-mini")) return 2e5;
13692
- if (m.includes("gemini-2")) return 1e6;
13693
- if (m.includes("gemini")) return 128e3;
13694
- if (m.includes("deepseek")) return 128e3;
13695
- return 2e5;
13696
- }
13697
- _gatewayStartHandler = async (event) => {
13698
- logger.debug("cohort-sync: hook: gateway_start", { port: event.port, eventKeys: Object.keys(event) });
13699
- try {
13700
- checkForUpdate(PLUGIN_VERSION, logger).catch(() => {
13701
- });
13702
- } catch {
13703
- }
13704
- try {
13705
- const agentList = (config?.agents?.list ?? []).map((a) => ({
13706
- id: a.id,
13707
- model: resolveModel(a.id),
13708
- identity: resolveIdentity(a.identity, a.workspace)
13709
- }));
13710
- await fullSync(resolveAgentName("main"), resolveModel("main"), cfg, logger, agentList);
13711
- } catch (err) {
13712
- logger.error(`cohort-sync: gateway_start sync failed: ${String(err)}`);
13713
- }
13714
- for (const a of config?.agents?.list ?? []) {
13715
- const agentName = resolveAgentName(a.id);
13716
- const mp = a.messageProvider;
13717
- logger.debug("cohort-sync: gateway start: seed bridge", { agentId: a.id, agentName, messageProvider: mp, accountId: a.accountId });
13718
- if (mp && typeof mp === "string") {
13719
- setChannelAgent(mp, agentName);
13720
- }
13721
- }
13722
- logger.debug("cohort-sync: gateway start: bridge after seed", { bridge: Object.fromEntries(getChannelAgentBridge()) });
13723
- gatewayPort = event.port;
13724
- const token = resolveGatewayToken(api);
13725
- if (token) {
13726
- gatewayToken = token;
13727
- logger.debug("cohort-sync: gateway client connecting", { port: event.port, hasToken: true });
13728
- try {
13729
- await initGatewayClient(event.port, token, cfg, resolveAgentName, logger);
13730
- if (commandUnsubscriber2) {
13731
- commandUnsubscriber2();
13732
- commandUnsubscriber2 = null;
13733
- }
13734
- const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
13735
- commandUnsubscriber2 = unsub;
13736
- } catch (err) {
13737
- logger.debug("cohort-sync: gateway client connect failed", { error: String(err) });
13738
- logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
13739
- }
13740
- } else {
13741
- logger.debug("cohort-sync: no gateway token");
13742
- logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
13743
- }
13744
- await startNotificationSubscription(
13745
- event.port,
13746
- cfg,
13747
- api.config.hooks?.token,
13748
- logger,
13749
- persistentGwClient
13750
- ).catch((err) => {
13751
- logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
13752
- });
13753
- const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
13754
- for (const agentId of allAgentIds) {
13755
- const agentName = resolveAgentName(agentId);
13756
- try {
13757
- tracker2.setModel(agentName, resolveModel(agentId));
13758
- tracker2.updateStatus(agentName, "idle");
13759
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13760
- if (snapshot) {
13761
- await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
13762
- tracker2.markTelemetryPushed(agentName);
13763
- }
13764
- } catch (err) {
13765
- logger.warn(`cohort-sync: initial telemetry seed failed for ${agentName}: ${String(err)}`);
13766
- }
13767
- }
13768
- logger.info(`cohort-sync: seeded telemetry for ${allAgentIds.length} agents`);
13769
- if (keepaliveInterval) clearInterval(keepaliveInterval);
13770
- keepaliveInterval = setInterval(async () => {
13771
- for (const agentId of allAgentIds) {
13772
- const agentName = resolveAgentName(agentId);
13773
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13774
- if (snapshot) {
13775
- await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION }).catch(() => {
13776
- });
13777
- }
13778
- }
13779
- for (const agentId of allAgentIds) {
13780
- const agentName = resolveAgentName(agentId);
13781
- tracker2.pruneStaleSessions(agentName, 864e5);
13782
- }
13783
- saveSessionsToDisk(tracker2);
13784
- }, KEEPALIVE_INTERVAL_MS);
13785
- logger.info(`cohort-sync: keepalive interval started (${KEEPALIVE_INTERVAL_MS / 1e3}s)`);
13786
- };
13787
13613
  api.registerHook("agent_end", async (event) => {
13614
+ const state = getState();
13615
+ if (!state) return;
13616
+ const { cfg, tracker, logger: log } = state;
13788
13617
  const ctx = event.context ?? {};
13789
- logger.debug("cohort-sync: hook: agent_end", { ctx: dumpCtx(ctx), success: ctx.success, error: ctx.error, durationMs: ctx.durationMs });
13618
+ log.debug("cohort-sync: hook: agent_end", { ctx: dumpCtx(ctx), success: ctx.success, error: ctx.error, durationMs: ctx.durationMs });
13790
13619
  const agentId = ctx.agentId ?? "main";
13791
- const agentName = resolveAgentName(agentId);
13620
+ const agentName = state.resolveAgentName(agentId);
13792
13621
  try {
13793
- tracker2.updateStatus(agentName, "idle");
13794
- await syncAgentStatus(agentName, "idle", resolveModel(agentId), cfg, logger);
13795
- if (tracker2.shouldPushTelemetry(agentName)) {
13796
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13622
+ tracker.updateStatus(agentName, "idle");
13623
+ await syncAgentStatus(agentName, "idle", state.resolveModel(agentId), cfg, log);
13624
+ if (tracker.shouldPushTelemetry(agentName)) {
13625
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
13797
13626
  if (snapshot) {
13798
13627
  await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
13799
- tracker2.markTelemetryPushed(agentName);
13628
+ tracker.markTelemetryPushed(agentName);
13800
13629
  }
13801
13630
  }
13802
13631
  const sessionKey = ctx.sessionKey;
13803
13632
  if (sessionKey && sessionKey.includes(":cron:")) {
13804
13633
  try {
13805
- const raw = fs2["read"+"FileSync"](cronStorePath, "utf8");
13634
+ const raw = fs2["read"+"FileSync"](state.cronStorePath, "utf8");
13806
13635
  const store = JSON.parse(raw);
13807
13636
  const jobs = store.jobs ?? [];
13808
- const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
13637
+ const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName));
13809
13638
  await pushCronSnapshot(cfg.apiKey, mapped);
13810
- logger.debug("cohort-sync: cron agent end push", { count: mapped.length, sessionKey });
13639
+ log.debug("cohort-sync: cron agent end push", { count: mapped.length, sessionKey });
13811
13640
  } catch (err) {
13812
- logger.debug("cohort-sync: cron agent end push failed", { error: String(err) });
13641
+ log.debug("cohort-sync: cron agent end push failed", { error: String(err) });
13813
13642
  }
13814
13643
  }
13815
13644
  if (ctx.success === false) {
@@ -13819,19 +13648,22 @@ function registerHooks(api, cfg) {
13819
13648
  durationMs: ctx.durationMs,
13820
13649
  sessionKey: ctx.sessionKey
13821
13650
  });
13822
- if (entry) activityBatch.add(entry);
13651
+ if (entry) state.activityBatch.add(entry);
13823
13652
  }
13824
13653
  } catch (err) {
13825
- logger.warn(`cohort-sync: agent_end sync failed: ${String(err)}`);
13654
+ log.warn(`cohort-sync: agent_end sync failed: ${String(err)}`);
13826
13655
  }
13827
13656
  }, { name: "cohort-sync.agent_end", description: "Mark idle, push telemetry, emit activity on failure" });
13828
13657
  api.registerHook("llm_output", async (event) => {
13658
+ const state = getState();
13659
+ if (!state) return;
13660
+ const { cfg, tracker, logger: log } = state;
13829
13661
  const ctx = event.context ?? {};
13830
13662
  const usage = ctx.usage ?? {};
13831
13663
  const contextTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
13832
- const model = ctx.model ?? resolveModel(ctx.agentId ?? "main");
13833
- const contextLimit = getModelContextLimit(model);
13834
- logger.debug("cohort-sync: hook: llm_output", {
13664
+ const model = ctx.model ?? state.resolveModel(ctx.agentId ?? "main");
13665
+ const contextLimit = state.getModelContextLimit(model);
13666
+ log.debug("cohort-sync: hook: llm_output", {
13835
13667
  ctx: dumpCtx(ctx),
13836
13668
  model,
13837
13669
  tokensIn: usage.input,
@@ -13842,54 +13674,57 @@ function registerHooks(api, cfg) {
13842
13674
  contextLimit
13843
13675
  });
13844
13676
  const agentId = ctx.agentId ?? "main";
13845
- const agentName = resolveAgentName(agentId);
13677
+ const agentName = state.resolveAgentName(agentId);
13846
13678
  try {
13847
13679
  const sessionKey = ctx.sessionKey;
13848
- tracker2.updateFromLlmOutput(agentName, sessionKey, {
13680
+ tracker.updateFromLlmOutput(agentName, sessionKey, {
13849
13681
  model,
13850
13682
  tokensIn: usage.input ?? 0,
13851
13683
  tokensOut: usage.output ?? 0,
13852
13684
  contextTokens,
13853
13685
  contextLimit
13854
13686
  });
13855
- if (sessionKey && !tracker2.hasSession(agentName, sessionKey)) {
13856
- tracker2.addSession(agentName, sessionKey);
13857
- logger.info(`cohort-sync: inferred session for ${agentName} from llm_output (${sessionKey})`);
13687
+ if (sessionKey && !tracker.hasSession(agentName, sessionKey)) {
13688
+ tracker.addSession(agentName, sessionKey);
13689
+ log.info(`cohort-sync: inferred session for ${agentName} from llm_output (${sessionKey})`);
13858
13690
  }
13859
13691
  if (sessionKey) {
13860
- tracker2.setSessionAgent(sessionKey, agentName);
13692
+ tracker.setSessionAgent(sessionKey, agentName);
13861
13693
  }
13862
- if (tracker2.shouldPushTelemetry(agentName)) {
13863
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13694
+ if (tracker.shouldPushTelemetry(agentName)) {
13695
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
13864
13696
  if (snapshot) {
13865
13697
  await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
13866
- tracker2.markTelemetryPushed(agentName);
13698
+ tracker.markTelemetryPushed(agentName);
13867
13699
  }
13868
13700
  }
13869
- if (tracker2.shouldPushSessions(agentName)) {
13870
- const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
13701
+ if (tracker.shouldPushSessions(agentName)) {
13702
+ const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
13871
13703
  await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
13872
- tracker2.markSessionsPushed(agentName);
13704
+ tracker.markSessionsPushed(agentName);
13873
13705
  }
13874
13706
  } catch (err) {
13875
- logger.warn(`cohort-sync: llm_output telemetry failed: ${String(err)}`);
13707
+ log.warn(`cohort-sync: llm_output telemetry failed: ${String(err)}`);
13876
13708
  }
13877
13709
  }, { name: "cohort-sync.llm_output", description: "Accumulate token usage, push telemetry and sessions" });
13878
13710
  api.registerHook("after_compaction", async (event) => {
13711
+ const state = getState();
13712
+ if (!state) return;
13713
+ const { cfg, tracker, logger: log } = state;
13879
13714
  const ctx = event.context ?? {};
13880
- logger.debug("cohort-sync: hook: after_compaction", { ctx: dumpCtx(ctx), messageCount: ctx.messageCount, tokenCount: ctx.tokenCount });
13715
+ log.debug("cohort-sync: hook: after_compaction", { ctx: dumpCtx(ctx), messageCount: ctx.messageCount, tokenCount: ctx.tokenCount });
13881
13716
  const agentId = ctx.agentId ?? "main";
13882
- const agentName = resolveAgentName(agentId);
13717
+ const agentName = state.resolveAgentName(agentId);
13883
13718
  try {
13884
- tracker2.updateFromCompaction(agentName, {
13719
+ tracker.updateFromCompaction(agentName, {
13885
13720
  contextTokens: ctx.tokenCount ?? 0,
13886
- contextLimit: getModelContextLimit(resolveModel(agentId))
13721
+ contextLimit: state.getModelContextLimit(state.resolveModel(agentId))
13887
13722
  });
13888
- if (tracker2.shouldPushTelemetry(agentName)) {
13889
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13723
+ if (tracker.shouldPushTelemetry(agentName)) {
13724
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
13890
13725
  if (snapshot) {
13891
13726
  await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
13892
- tracker2.markTelemetryPushed(agentName);
13727
+ tracker.markTelemetryPushed(agentName);
13893
13728
  }
13894
13729
  }
13895
13730
  const entry = buildActivityEntry(agentName, "after_compaction", {
@@ -13897,51 +13732,56 @@ function registerHooks(api, cfg) {
13897
13732
  compactedCount: ctx.compactedCount,
13898
13733
  sessionKey: ctx.sessionKey
13899
13734
  });
13900
- if (entry) activityBatch.add(entry);
13735
+ if (entry) state.activityBatch.add(entry);
13901
13736
  } catch (err) {
13902
- logger.warn(`cohort-sync: after_compaction telemetry failed: ${String(err)}`);
13737
+ log.warn(`cohort-sync: after_compaction telemetry failed: ${String(err)}`);
13903
13738
  }
13904
13739
  }, { name: "cohort-sync.after_compaction", description: "Increment compaction count, push telemetry" });
13905
13740
  api.registerHook("before_agent_start", async (event) => {
13741
+ const state = getState();
13742
+ if (!state) return;
13743
+ const { cfg, tracker, logger: log } = state;
13906
13744
  const ctx = event.context ?? {};
13907
- logger.debug("cohort-sync: hook: before_agent_start", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13745
+ log.debug("cohort-sync: hook: before_agent_start", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13908
13746
  const agentId = ctx.agentId ?? "main";
13909
- const agentName = resolveAgentName(agentId);
13910
- logger.debug("cohort-sync: hook: before_agent_start resolved", { agentId, agentName, ctxChannelId: ctx.channelId, ctxMessageProvider: ctx.messageProvider, ctxSessionKey: ctx.sessionKey, ctxAccountId: ctx.accountId });
13747
+ const agentName = state.resolveAgentName(agentId);
13748
+ log.debug("cohort-sync: hook: before_agent_start resolved", { agentId, agentName, ctxChannelId: ctx.channelId, ctxMessageProvider: ctx.messageProvider, ctxSessionKey: ctx.sessionKey, ctxAccountId: ctx.accountId });
13911
13749
  try {
13912
- if (!gwClientInitialized && gatewayPort && gatewayToken) {
13750
+ if (!state.gwClientInitialized && state.gatewayPort && state.gatewayToken) {
13913
13751
  try {
13914
- await initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
13915
- if (commandUnsubscriber2) {
13916
- commandUnsubscriber2();
13917
- commandUnsubscriber2 = null;
13752
+ const client2 = initGatewayClient(state.gatewayPort, state.gatewayToken, cfg, state.resolveAgentName, log);
13753
+ state.persistentGwClient = client2;
13754
+ state.gwClientInitialized = true;
13755
+ if (state.commandUnsubscriber) {
13756
+ state.commandUnsubscriber();
13757
+ state.commandUnsubscriber = null;
13918
13758
  }
13919
- const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
13920
- commandUnsubscriber2 = unsub;
13759
+ const unsub = startCommandSubscription(cfg, log, state.resolveAgentName, state.persistentGwClient);
13760
+ state.commandUnsubscriber = unsub;
13921
13761
  } catch (err) {
13922
- logger.debug("cohort-sync: gateway client lazy init failed", { error: String(err) });
13762
+ log.debug("cohort-sync: gateway client lazy init failed", { error: String(err) });
13923
13763
  }
13924
13764
  }
13925
- tracker2.updateStatus(agentName, "working");
13926
- if (tracker2.shouldPushTelemetry(agentName)) {
13927
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13765
+ tracker.updateStatus(agentName, "working");
13766
+ if (tracker.shouldPushTelemetry(agentName)) {
13767
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
13928
13768
  if (snapshot) {
13929
13769
  await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
13930
- tracker2.markTelemetryPushed(agentName);
13770
+ tracker.markTelemetryPushed(agentName);
13931
13771
  }
13932
13772
  }
13933
13773
  const sessionKey = ctx.sessionKey;
13934
- if (sessionKey && !tracker2.hasSession(agentName, sessionKey)) {
13935
- tracker2.addSession(agentName, sessionKey);
13936
- tracker2.setSessionAgent(sessionKey, agentName);
13937
- logger.info(`cohort-sync: inferred session for ${agentName} (${sessionKey})`);
13938
- if (tracker2.shouldPushSessions(agentName)) {
13939
- const sessSnapshot = tracker2.getSessionsSnapshot(agentName);
13774
+ if (sessionKey && !tracker.hasSession(agentName, sessionKey)) {
13775
+ tracker.addSession(agentName, sessionKey);
13776
+ tracker.setSessionAgent(sessionKey, agentName);
13777
+ log.info(`cohort-sync: inferred session for ${agentName} (${sessionKey})`);
13778
+ if (tracker.shouldPushSessions(agentName)) {
13779
+ const sessSnapshot = tracker.getSessionsSnapshot(agentName);
13940
13780
  await pushSessions(cfg.apiKey, agentName, sessSnapshot);
13941
- tracker2.markSessionsPushed(agentName);
13781
+ tracker.markSessionsPushed(agentName);
13942
13782
  }
13943
13783
  } else if (sessionKey) {
13944
- tracker2.setSessionAgent(sessionKey, agentName);
13784
+ tracker.setSessionAgent(sessionKey, agentName);
13945
13785
  }
13946
13786
  const ctxChannelId = ctx.channelId;
13947
13787
  if (ctxChannelId) {
@@ -13952,21 +13792,24 @@ function registerHooks(api, cfg) {
13952
13792
  setChannelAgent(mp, agentName);
13953
13793
  }
13954
13794
  } catch (err) {
13955
- logger.warn(`cohort-sync: before_agent_start telemetry failed: ${String(err)}`);
13795
+ log.warn(`cohort-sync: before_agent_start telemetry failed: ${String(err)}`);
13956
13796
  }
13957
13797
  }, { name: "cohort-sync.before_agent_start", description: "Mark working, infer sessions, populate channel bridge" });
13958
13798
  api.registerHook("session_start", async (event) => {
13799
+ const state = getState();
13800
+ if (!state) return;
13801
+ const { cfg, tracker, logger: log } = state;
13959
13802
  const ctx = event.context ?? {};
13960
- logger.debug("cohort-sync: hook: session_start", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13803
+ log.debug("cohort-sync: hook: session_start", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13961
13804
  const agentId = ctx.agentId ?? "main";
13962
- const agentName = resolveAgentName(agentId);
13805
+ const agentName = state.resolveAgentName(agentId);
13963
13806
  try {
13964
13807
  const sessionKey = ctx.sessionId ?? String(Date.now());
13965
- tracker2.addSession(agentName, sessionKey);
13966
- if (tracker2.shouldPushSessions(agentName)) {
13967
- const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
13808
+ tracker.addSession(agentName, sessionKey);
13809
+ if (tracker.shouldPushSessions(agentName)) {
13810
+ const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
13968
13811
  await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
13969
- tracker2.markSessionsPushed(agentName);
13812
+ tracker.markSessionsPushed(agentName);
13970
13813
  }
13971
13814
  const parsed = parseSessionKey(sessionKey);
13972
13815
  const entry = buildActivityEntry(agentName, "session_start", {
@@ -13974,38 +13817,43 @@ function registerHooks(api, cfg) {
13974
13817
  sessionKey,
13975
13818
  resumedFrom: ctx.resumedFrom
13976
13819
  });
13977
- if (entry) activityBatch.add(entry);
13820
+ if (entry) state.activityBatch.add(entry);
13978
13821
  } catch (err) {
13979
- logger.warn(`cohort-sync: session_start tracking failed: ${String(err)}`);
13822
+ log.warn(`cohort-sync: session_start tracking failed: ${String(err)}`);
13980
13823
  }
13981
13824
  }, { name: "cohort-sync.session_start", description: "Track session start, push sessions" });
13982
13825
  api.registerHook("session_end", async (event) => {
13826
+ const state = getState();
13827
+ if (!state) return;
13828
+ const { cfg, tracker, logger: log } = state;
13983
13829
  const ctx = event.context ?? {};
13984
- logger.debug("cohort-sync: hook: session_end", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13830
+ log.debug("cohort-sync: hook: session_end", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13985
13831
  const agentId = ctx.agentId ?? "main";
13986
- const agentName = resolveAgentName(agentId);
13832
+ const agentName = state.resolveAgentName(agentId);
13987
13833
  try {
13988
13834
  const sessionKey = ctx.sessionId ?? "";
13989
- tracker2.removeSession(agentName, sessionKey);
13990
- if (tracker2.shouldPushSessions(agentName)) {
13991
- const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
13835
+ tracker.removeSession(agentName, sessionKey);
13836
+ if (tracker.shouldPushSessions(agentName)) {
13837
+ const sessionsSnapshot = tracker.getSessionsSnapshot(agentName);
13992
13838
  await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
13993
- tracker2.markSessionsPushed(agentName);
13839
+ tracker.markSessionsPushed(agentName);
13994
13840
  }
13995
13841
  const entry = buildActivityEntry(agentName, "session_end", {
13996
13842
  sessionKey,
13997
13843
  messageCount: ctx.messageCount,
13998
13844
  durationMs: ctx.durationMs
13999
13845
  });
14000
- if (entry) activityBatch.add(entry);
13846
+ if (entry) state.activityBatch.add(entry);
14001
13847
  } catch (err) {
14002
- logger.warn(`cohort-sync: session_end tracking failed: ${String(err)}`);
13848
+ log.warn(`cohort-sync: session_end tracking failed: ${String(err)}`);
14003
13849
  }
14004
13850
  }, { name: "cohort-sync.session_end", description: "Remove session, push sessions" });
14005
13851
  api.registerHook("after_tool_call", async (event) => {
13852
+ const state = getState();
13853
+ if (!state) return;
14006
13854
  const ctx = event.context ?? {};
14007
- logger.debug("cohort-sync: hook: after_tool_call", { ctx: dumpCtx(ctx), toolName: ctx.toolName, error: ctx.error });
14008
- const agentName = resolveAgentFromContext(ctx);
13855
+ state.logger.debug("cohort-sync: hook: after_tool_call", { ctx: dumpCtx(ctx), toolName: ctx.toolName, error: ctx.error });
13856
+ const agentName = resolveAgentFromContext(state, ctx);
14009
13857
  try {
14010
13858
  const entry = buildActivityEntry(agentName, "after_tool_call", {
14011
13859
  toolName: ctx.toolName,
@@ -14013,23 +13861,25 @@ function registerHooks(api, cfg) {
14013
13861
  error: ctx.error,
14014
13862
  durationMs: ctx.durationMs,
14015
13863
  sessionKey: ctx.sessionKey,
14016
- model: resolveModel(ctx.agentId ?? "main")
13864
+ model: state.resolveModel(ctx.agentId ?? "main")
14017
13865
  });
14018
- if (entry) activityBatch.add(entry);
13866
+ if (entry) state.activityBatch.add(entry);
14019
13867
  } catch (err) {
14020
- logger.warn(`cohort-sync: after_tool_call activity failed: ${String(err)}`);
13868
+ state.logger.warn(`cohort-sync: after_tool_call activity failed: ${String(err)}`);
14021
13869
  }
14022
13870
  }, { name: "cohort-sync.after_tool_call", description: "Emit activity entry for tool calls" });
14023
13871
  api.registerHook("message_received", async (event) => {
13872
+ const state = getState();
13873
+ if (!state) return;
14024
13874
  const ctx = event.context ?? {};
14025
- logger.debug("cohort-sync: hook: message_received raw", {
13875
+ state.logger.debug("cohort-sync: hook: message_received raw", {
14026
13876
  ctx: dumpCtx(ctx),
14027
13877
  event: dumpEvent(event),
14028
13878
  bridgeStateBefore: Object.fromEntries(getChannelAgentBridge())
14029
13879
  });
14030
- const agentName = resolveAgentFromContext(ctx);
13880
+ const agentName = resolveAgentFromContext(state, ctx);
14031
13881
  const channel = ctx.channelId;
14032
- logger.debug("cohort-sync: hook: message_received resolved", {
13882
+ state.logger.debug("cohort-sync: hook: message_received resolved", {
14033
13883
  agentName,
14034
13884
  channel,
14035
13885
  accountId: ctx.accountId,
@@ -14040,21 +13890,23 @@ function registerHooks(api, cfg) {
14040
13890
  const entry = buildActivityEntry(agentName, "message_received", {
14041
13891
  channel: channel ?? "unknown"
14042
13892
  });
14043
- if (entry) activityBatch.add(entry);
13893
+ if (entry) state.activityBatch.add(entry);
14044
13894
  } catch (err) {
14045
- logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
13895
+ state.logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
14046
13896
  }
14047
13897
  }, { name: "cohort-sync.message_received", description: "Emit activity entry for received messages" });
14048
13898
  api.registerHook("message_sent", async (event) => {
13899
+ const state = getState();
13900
+ if (!state) return;
14049
13901
  const ctx = event.context ?? {};
14050
- logger.debug("cohort-sync: hook: message_sent raw", {
13902
+ state.logger.debug("cohort-sync: hook: message_sent raw", {
14051
13903
  ctx: dumpCtx(ctx),
14052
13904
  event: dumpEvent(event),
14053
13905
  bridgeStateBefore: Object.fromEntries(getChannelAgentBridge())
14054
13906
  });
14055
- const agentName = resolveAgentFromContext(ctx);
13907
+ const agentName = resolveAgentFromContext(state, ctx);
14056
13908
  const channel = ctx.channelId;
14057
- logger.debug("cohort-sync: hook: message_sent resolved", {
13909
+ state.logger.debug("cohort-sync: hook: message_sent resolved", {
14058
13910
  agentName,
14059
13911
  channel,
14060
13912
  accountId: ctx.accountId,
@@ -14069,76 +13921,240 @@ function registerHooks(api, cfg) {
14069
13921
  success: ctx.success,
14070
13922
  error: ctx.error
14071
13923
  });
14072
- if (entry) activityBatch.add(entry);
13924
+ if (entry) state.activityBatch.add(entry);
14073
13925
  } catch (err) {
14074
- logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
13926
+ state.logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
14075
13927
  }
14076
13928
  }, { name: "cohort-sync.message_sent", description: "Emit activity entry for sent messages" });
14077
13929
  api.registerHook("before_compaction", async (event) => {
13930
+ const state = getState();
13931
+ if (!state) return;
14078
13932
  const ctx = event.context ?? {};
14079
- logger.debug("cohort-sync: hook: before_compaction", { ctx: dumpCtx(ctx) });
13933
+ state.logger.debug("cohort-sync: hook: before_compaction", { ctx: dumpCtx(ctx) });
14080
13934
  const agentId = ctx.agentId ?? "main";
14081
- const agentName = resolveAgentName(agentId);
13935
+ const agentName = state.resolveAgentName(agentId);
14082
13936
  try {
14083
13937
  const entry = buildActivityEntry(agentName, "before_compaction", {
14084
13938
  sessionKey: ctx.sessionKey
14085
13939
  });
14086
- if (entry) activityBatch.add(entry);
13940
+ if (entry) state.activityBatch.add(entry);
14087
13941
  } catch (err) {
14088
- logger.warn(`cohort-sync: before_compaction activity failed: ${String(err)}`);
13942
+ state.logger.warn(`cohort-sync: before_compaction activity failed: ${String(err)}`);
14089
13943
  }
14090
13944
  }, { name: "cohort-sync.before_compaction", description: "Emit activity entry before compaction" });
14091
13945
  api.registerHook("before_reset", async (event) => {
13946
+ const state = getState();
13947
+ if (!state) return;
14092
13948
  const ctx = event.context ?? {};
14093
- logger.debug("cohort-sync: hook: before_reset", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
13949
+ state.logger.debug("cohort-sync: hook: before_reset", { ctx: dumpCtx(ctx), event: dumpEvent(event) });
14094
13950
  const agentId = ctx.agentId ?? "main";
14095
- const agentName = resolveAgentName(agentId);
13951
+ const agentName = state.resolveAgentName(agentId);
14096
13952
  try {
14097
13953
  const entry = buildActivityEntry(agentName, "before_reset", {
14098
13954
  reason: ctx.reason,
14099
13955
  sessionKey: ctx.sessionKey
14100
13956
  });
14101
- if (entry) activityBatch.add(entry);
13957
+ if (entry) state.activityBatch.add(entry);
14102
13958
  } catch (err) {
14103
- logger.warn(`cohort-sync: before_reset activity failed: ${String(err)}`);
13959
+ state.logger.warn(`cohort-sync: before_reset activity failed: ${String(err)}`);
14104
13960
  }
14105
13961
  }, { name: "cohort-sync.before_reset", description: "Emit activity entry before reset" });
14106
13962
  api.registerHook("gateway_stop", async () => {
14107
- logger.debug("cohort-sync: hook: gateway_stop", { bridgeState: Object.fromEntries(getChannelAgentBridge()) });
14108
- if (keepaliveInterval) {
14109
- clearInterval(keepaliveInterval);
14110
- keepaliveInterval = null;
14111
- }
14112
- activityBatch.drain();
13963
+ const state = getState();
13964
+ if (!state) return;
13965
+ const { cfg, tracker, logger: log, config } = state;
13966
+ log.debug("cohort-sync: hook: gateway_stop", { bridgeState: Object.fromEntries(getChannelAgentBridge()) });
13967
+ if (state.keepaliveInterval) {
13968
+ clearInterval(state.keepaliveInterval);
13969
+ state.keepaliveInterval = null;
13970
+ }
13971
+ state.activityBatch.drain();
14113
13972
  const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
14114
13973
  for (const agentId of allAgentIds) {
14115
- const agentName = resolveAgentName(agentId);
13974
+ const agentName = state.resolveAgentName(agentId);
14116
13975
  try {
14117
- tracker2.updateStatus(agentName, "unreachable");
14118
- const snapshot = tracker2.getTelemetrySnapshot(agentName);
13976
+ tracker.updateStatus(agentName, "unreachable");
13977
+ const snapshot = tracker.getTelemetrySnapshot(agentName);
14119
13978
  if (snapshot) {
14120
13979
  await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
14121
13980
  }
14122
13981
  } catch (err) {
14123
- logger.warn(`cohort-sync: final unreachable push failed for ${agentName}: ${String(err)}`);
13982
+ log.warn(`cohort-sync: final unreachable push failed for ${agentName}: ${String(err)}`);
14124
13983
  }
14125
13984
  }
14126
- saveSessionsToDisk(tracker2);
14127
- if (persistentGwClient) {
14128
- persistentGwClient.close();
14129
- persistentGwClient = null;
14130
- gwClientInitialized = false;
13985
+ saveSessionsToDisk(tracker, state.stateFilePath);
13986
+ if (state.persistentGwClient) {
13987
+ state.persistentGwClient.close();
13988
+ state.persistentGwClient = null;
13989
+ state.gwClientInitialized = false;
14131
13990
  }
14132
13991
  try {
14133
- await markAllUnreachable(cfg, logger);
13992
+ await markAllUnreachable(cfg, log);
14134
13993
  } catch (err) {
14135
- logger.warn(`cohort-sync: markAllUnreachable failed: ${String(err)}`);
13994
+ log.warn(`cohort-sync: markAllUnreachable failed: ${String(err)}`);
14136
13995
  }
14137
- tracker2.clear();
13996
+ tracker.clear();
14138
13997
  closeBridge();
14139
- logger.info("cohort-sync: gateway stopped, all resources cleaned up");
13998
+ log.info("cohort-sync: gateway stopped, all resources cleaned up");
14140
13999
  }, { name: "cohort-sync.gateway_stop", description: "Cleanup, final telemetry, mark unreachable" });
14141
14000
  }
14001
+ function initializeHookState(api, cfg) {
14002
+ const { logger, config } = api;
14003
+ const stateFilePath = path2.join(cfg.stateDir, "session-state.json");
14004
+ const nameMap = cfg.agentNameMap;
14005
+ const tracker = new AgentStateTracker();
14006
+ const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
14007
+ createClient(convexUrl);
14008
+ setLogger(logger);
14009
+ const identityNameMap = {};
14010
+ const mainIdentity = parseIdentityFile(process.cwd());
14011
+ if (mainIdentity?.name) {
14012
+ identityNameMap["main"] = mainIdentity.name.toLowerCase();
14013
+ }
14014
+ for (const agent of config?.agents?.list ?? []) {
14015
+ const identity = resolveIdentity(agent.identity, agent.workspace);
14016
+ if (identity?.name) {
14017
+ identityNameMap[agent.id] = identity.name.toLowerCase();
14018
+ }
14019
+ }
14020
+ logger.debug("cohort-sync: identity name map", { identityNameMap });
14021
+ function resolveAgentName(agentId) {
14022
+ return (nameMap?.[agentId] ?? identityNameMap[agentId] ?? agentId).toLowerCase();
14023
+ }
14024
+ function resolveModel(agentId) {
14025
+ const agent = config?.agents?.list?.find((a) => a.id === agentId);
14026
+ const m = agent?.model;
14027
+ if (typeof m === "string") return m;
14028
+ if (m && typeof m === "object" && "primary" in m) return String(m.primary);
14029
+ return "unknown";
14030
+ }
14031
+ function getModelContextLimit(model) {
14032
+ const m = model.toLowerCase();
14033
+ if (m.includes("opus") || m.includes("sonnet") || m.includes("haiku")) return 2e5;
14034
+ if (m.includes("gpt-4o")) return 128e3;
14035
+ if (m.includes("gpt-4-turbo") || m.includes("gpt-4-1")) return 128e3;
14036
+ if (m.includes("gpt-4")) return 8192;
14037
+ if (m.includes("o3") || m.includes("o4-mini")) return 2e5;
14038
+ if (m.includes("gemini-2")) return 1e6;
14039
+ if (m.includes("gemini")) return 128e3;
14040
+ if (m.includes("deepseek")) return 128e3;
14041
+ return 2e5;
14042
+ }
14043
+ const cronStorePath = api.config?.cron?.store ?? path2.join(os2.homedir(), ".openclaw", "cron", "jobs.json");
14044
+ const activityBatch = new MicroBatch({
14045
+ maxSize: 10,
14046
+ maxDelayMs: 1e3,
14047
+ flush: (entries) => pushActivity(cfg.apiKey, entries),
14048
+ onError: (err) => logger.warn(`cohort-sync: activity batch flush failed: ${String(err)}`)
14049
+ });
14050
+ const gatewayPort = api.config?.gateway?.port ?? null;
14051
+ const gatewayToken = resolveGatewayToken(api);
14052
+ let persistentGwClient = null;
14053
+ let gwClientInitialized = false;
14054
+ if (gatewayPort && gatewayToken) {
14055
+ const client2 = initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
14056
+ persistentGwClient = client2;
14057
+ gwClientInitialized = true;
14058
+ }
14059
+ const commandUnsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
14060
+ setToolRuntime({
14061
+ apiKey: cfg.apiKey,
14062
+ apiUrl: cfg.apiUrl,
14063
+ resolveAgentName,
14064
+ logger
14065
+ });
14066
+ if (tracker.getAgentNames().length === 0) {
14067
+ loadSessionsFromDisk(tracker, stateFilePath, logger);
14068
+ const restoredAgents = tracker.getAgentNames();
14069
+ for (const agentName of restoredAgents) {
14070
+ const sessSnapshot = tracker.getSessionsSnapshot(agentName);
14071
+ if (sessSnapshot.length > 0) {
14072
+ pushSessions(cfg.apiKey, agentName, sessSnapshot).then(() => {
14073
+ tracker.markSessionsPushed(agentName);
14074
+ logger.info(`cohort-sync: pushed ${sessSnapshot.length} restored sessions for ${agentName}`);
14075
+ }).catch((err) => {
14076
+ logger.warn(`cohort-sync: failed to push restored sessions for ${agentName}: ${String(err)}`);
14077
+ });
14078
+ }
14079
+ }
14080
+ }
14081
+ logger.info(`cohort-sync: initializeHookState v${PLUGIN_VERSION}`);
14082
+ logger.info("cohort-sync: state initialized", {
14083
+ PLUGIN_VERSION,
14084
+ hasNameMap: !!nameMap,
14085
+ nameMapKeys: nameMap ? Object.keys(nameMap) : [],
14086
+ nameMapValues: nameMap ? Object.values(nameMap) : [],
14087
+ agentCount: config?.agents?.list?.length ?? 0,
14088
+ agentIds: (config?.agents?.list ?? []).map((a) => a.id),
14089
+ agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
14090
+ });
14091
+ return {
14092
+ cfg,
14093
+ tracker,
14094
+ logger,
14095
+ config,
14096
+ resolveAgentName,
14097
+ resolveModel,
14098
+ getModelContextLimit,
14099
+ activityBatch,
14100
+ cronStorePath,
14101
+ stateFilePath,
14102
+ gatewayPort,
14103
+ gatewayToken,
14104
+ persistentGwClient,
14105
+ gwClientInitialized,
14106
+ keepaliveInterval: null,
14107
+ commandUnsubscriber: commandUnsub,
14108
+ api
14109
+ };
14110
+ }
14111
+
14112
+ // src/pocket-guide.ts
14113
+ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
14114
+
14115
+ > These are the essential rules. For the full guide, see https://docs.cohort.bot/integration/guide
14116
+
14117
+ ## Getting Started
14118
+ - Call the cohort_context tool at the start of every work session for your personalized briefing.
14119
+ - If cohort_context is unavailable, call GET /api/v1/context directly.
14120
+ - Do not skip the context call \u2014 it contains workspace-specific overrides that supersede these rules.
14121
+
14122
+ ## Task Lifecycle
14123
+ - Create tasks for trackable work items. Always set priority (default p2) and effort when estimable.
14124
+ - Use POST /tasks/:id/transition for status changes. Never PATCH status directly \u2014 the server rejects it.
14125
+ - If a transition is rejected, read the error response for valid transitions from the current status.
14126
+ - You cannot transition tasks to "done" unless your workspace admin has enabled it. Only humans close tasks by default.
14127
+ - When moving to "in_progress", you are claiming ownership \u2014 only claim tasks you can actively work on.
14128
+ - When moving to "waiting", comment what you are blocked on and who/what can unblock you.
14129
+ - When work is complete, comment with evidence: PR link, test output, or summary of changes.
14130
+
14131
+ ## Comments
14132
+ - Comment before every status transition explaining what happened.
14133
+ - Post progress updates every 15-30 minutes on long-running work.
14134
+ - Keep comments factual \u2014 what you did, what you found, what is next.
14135
+ - No conversational filler. Every comment must contain information.
14136
+ - Reference specific files, line numbers, error messages, or URLs when relevant.
14137
+
14138
+ ## Projects & Initiatives
14139
+ - Don't create projects or initiatives without explicit instruction.
14140
+ - Assign tasks to existing projects when one fits. Don't create a project for a single task.
14141
+ - If no project fits, leave the task unassigned to a project.
14142
+
14143
+ ## Error Recovery
14144
+ - If auth fails (401), stop immediately. Do not retry. Report the failure.
14145
+ - If rate limited (429), wait per Retry-After header.
14146
+ - If 404, verify you are using the correct task number or ID (both accepted).
14147
+ - If 500, retry once after a brief pause. If it fails again, stop and report.
14148
+
14149
+ ## What Not To Do
14150
+ - Don't poll /tasks in a loop \u2014 check assignments, then do your work.
14151
+ - Don't create duplicate tasks \u2014 search first.
14152
+ - Don't delete tasks unless explicitly told to.
14153
+ - Don't bulk-create tasks speculatively \u2014 create them as work becomes concrete.
14154
+ - Don't modify tasks assigned to other agents unless coordinating through comments.
14155
+ - Don't set "done" on tasks you didn't work on.
14156
+ - Don't ignore workspace-specific overrides from your context response.
14157
+ `;
14142
14158
 
14143
14159
  // index.ts
14144
14160
  function textResult(text, details) {
@@ -14166,14 +14182,16 @@ var plugin = {
14166
14182
  );
14167
14183
  return;
14168
14184
  }
14169
- const gatewayPort2 = api.config?.gateway?.port ?? 18789;
14185
+ let hookState = null;
14186
+ registerHookHandlers(api, api.logger, () => hookState);
14187
+ const gatewayPort = api.config?.gateway?.port ?? 18789;
14170
14188
  api.registerHook(
14171
14189
  "gateway:startup",
14172
14190
  async (...args) => {
14173
14191
  const event = args[0] ?? {};
14174
- const port = event?.port ?? gatewayPort2;
14192
+ const port = event?.port ?? gatewayPort;
14175
14193
  api.logger.info(`cohort-sync: gateway:startup hook fired (port=${port})`);
14176
- await handleGatewayStart({ ...event, port });
14194
+ await handleGatewayStart({ ...event, port }, hookState);
14177
14195
  },
14178
14196
  {
14179
14197
  name: "cohort-sync.gateway-startup",
@@ -14420,7 +14438,7 @@ Do not attempt more comments until tomorrow.`);
14420
14438
  return;
14421
14439
  }
14422
14440
  api.logger.info(`cohort-sync: activated (api: ${apiUrl2})`);
14423
- registerHooks(api, {
14441
+ hookState = initializeHookState(api, {
14424
14442
  apiUrl: apiUrl2,
14425
14443
  apiKey: apiKey2,
14426
14444
  stateDir: svcCtx.stateDir,
@@ -14428,6 +14446,7 @@ Do not attempt more comments until tomorrow.`);
14428
14446
  });
14429
14447
  },
14430
14448
  async stop() {
14449
+ hookState = null;
14431
14450
  closeBridge();
14432
14451
  api.logger.info("cohort-sync: service stopped");
14433
14452
  }
@@ -55,5 +55,5 @@
55
55
  }
56
56
  }
57
57
  },
58
- "version": "0.10.3"
58
+ "version": "0.11.0"
59
59
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",