@cfio/cohort-sync 0.6.0 → 0.8.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 +916 -928
- package/dist/openclaw.plugin.json +11 -1
- package/dist/package.json +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,14 @@ __export(keychain_exports, {
|
|
|
16
16
|
setCredential: () => setCredential
|
|
17
17
|
});
|
|
18
18
|
import { execFile } from "node:child_process";
|
|
19
|
+
import os3 from "node:os";
|
|
20
|
+
function assertMacOS(operation) {
|
|
21
|
+
if (os3.platform() !== "darwin") {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`cohort-sync: ${operation} requires macOS Keychain. On Linux/Windows, set your API key in OpenClaw config: plugins.entries.cohort-sync.config.apiKey`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
19
27
|
function securityCmd(args) {
|
|
20
28
|
return new Promise((resolve, reject) => {
|
|
21
29
|
execFile("security", args, { timeout: 5e3 }, (err, stdout, stderr) => {
|
|
@@ -37,6 +45,7 @@ function isNotFoundError(err) {
|
|
|
37
45
|
return false;
|
|
38
46
|
}
|
|
39
47
|
async function setCredential(apiUrl, apiKey) {
|
|
48
|
+
assertMacOS("storing credentials");
|
|
40
49
|
await securityCmd([
|
|
41
50
|
"add-generic-password",
|
|
42
51
|
"-s",
|
|
@@ -49,6 +58,7 @@ async function setCredential(apiUrl, apiKey) {
|
|
|
49
58
|
]);
|
|
50
59
|
}
|
|
51
60
|
async function getCredential(apiUrl) {
|
|
61
|
+
assertMacOS("reading credentials");
|
|
52
62
|
try {
|
|
53
63
|
const { stdout } = await securityCmd([
|
|
54
64
|
"find-generic-password",
|
|
@@ -65,6 +75,7 @@ async function getCredential(apiUrl) {
|
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
async function deleteCredential(apiUrl) {
|
|
78
|
+
assertMacOS("deleting credentials");
|
|
68
79
|
try {
|
|
69
80
|
await securityCmd([
|
|
70
81
|
"delete-generic-password",
|
|
@@ -2699,6 +2710,225 @@ var Type = type_exports2;
|
|
|
2699
2710
|
|
|
2700
2711
|
// src/sync.ts
|
|
2701
2712
|
import { execSync } from "node:child_process";
|
|
2713
|
+
function extractJson(raw) {
|
|
2714
|
+
const jsonStart = raw.search(/[\[{]/);
|
|
2715
|
+
const jsonEndBracket = raw.lastIndexOf("]");
|
|
2716
|
+
const jsonEndBrace = raw.lastIndexOf("}");
|
|
2717
|
+
const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
|
|
2718
|
+
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
|
|
2719
|
+
throw new Error("No JSON found in output");
|
|
2720
|
+
}
|
|
2721
|
+
return raw.slice(jsonStart, jsonEnd + 1);
|
|
2722
|
+
}
|
|
2723
|
+
function fetchSkills(logger) {
|
|
2724
|
+
try {
|
|
2725
|
+
const raw = execSync("openclaw skills list --json", {
|
|
2726
|
+
encoding: "utf8",
|
|
2727
|
+
timeout: 3e4,
|
|
2728
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2729
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
2730
|
+
});
|
|
2731
|
+
const parsed = JSON.parse(extractJson(raw));
|
|
2732
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.skills ?? [];
|
|
2733
|
+
return list.map((s) => ({
|
|
2734
|
+
name: String(s.name ?? s.id ?? "unknown"),
|
|
2735
|
+
description: String(s.description ?? ""),
|
|
2736
|
+
source: String(s.source ?? s.origin ?? "unknown"),
|
|
2737
|
+
...s.emoji ? { emoji: String(s.emoji) } : {}
|
|
2738
|
+
}));
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
logger.warn(`cohort-sync: failed to fetch skills: ${String(err)}`);
|
|
2741
|
+
return [];
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
2745
|
+
function normalizeStatus(status) {
|
|
2746
|
+
return VALID_STATUSES.has(status) ? status : "idle";
|
|
2747
|
+
}
|
|
2748
|
+
async function v1Get(apiUrl, apiKey, path4) {
|
|
2749
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
2750
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
2751
|
+
signal: AbortSignal.timeout(1e4)
|
|
2752
|
+
});
|
|
2753
|
+
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
2754
|
+
return res.json();
|
|
2755
|
+
}
|
|
2756
|
+
async function v1Patch(apiUrl, apiKey, path4, body) {
|
|
2757
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
2758
|
+
method: "PATCH",
|
|
2759
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
2760
|
+
body: JSON.stringify(body),
|
|
2761
|
+
signal: AbortSignal.timeout(1e4)
|
|
2762
|
+
});
|
|
2763
|
+
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
2764
|
+
}
|
|
2765
|
+
async function v1Post(apiUrl, apiKey, path4, body) {
|
|
2766
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
2767
|
+
method: "POST",
|
|
2768
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
2769
|
+
body: JSON.stringify(body),
|
|
2770
|
+
signal: AbortSignal.timeout(1e4)
|
|
2771
|
+
});
|
|
2772
|
+
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
2773
|
+
}
|
|
2774
|
+
async function checkForUpdate(currentVersion, logger) {
|
|
2775
|
+
try {
|
|
2776
|
+
const res = await fetch("https://registry.npmjs.org/@cfio/cohort-sync/latest", {
|
|
2777
|
+
signal: AbortSignal.timeout(5e3)
|
|
2778
|
+
});
|
|
2779
|
+
if (!res.ok) return;
|
|
2780
|
+
const data = await res.json();
|
|
2781
|
+
const latest = data.version;
|
|
2782
|
+
if (latest && latest !== currentVersion) {
|
|
2783
|
+
logger.warn(
|
|
2784
|
+
`cohort-sync: update available (${currentVersion} \u2192 ${latest}) \u2014 run "npm install -g @cfio/cohort-sync" to update`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
} catch {
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
async function syncAgentStatus(agentName, status, model, cfg, logger) {
|
|
2791
|
+
try {
|
|
2792
|
+
const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
2793
|
+
const agents = data?.data ?? [];
|
|
2794
|
+
const agent = agents.find(
|
|
2795
|
+
(a) => a.name.toLowerCase() === agentName.toLowerCase()
|
|
2796
|
+
);
|
|
2797
|
+
if (!agent) {
|
|
2798
|
+
const available = agents.map((a) => a.name).join(", ") || "(none)";
|
|
2799
|
+
logger.warn(
|
|
2800
|
+
`cohort-sync: agent "${agentName}" not found in workspace \u2014 skipping status sync. Available agents: [${available}]. Configure agentNameMap in your plugin config to map OpenClaw agent IDs to Cohort names.`
|
|
2801
|
+
);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
2805
|
+
status: normalizeStatus(status),
|
|
2806
|
+
model
|
|
2807
|
+
});
|
|
2808
|
+
logger.info(`cohort-sync: agent "${agentName}" \u2192 ${normalizeStatus(status)}`);
|
|
2809
|
+
} catch (err) {
|
|
2810
|
+
logger.warn(`cohort-sync: syncAgentStatus failed: ${String(err)}`);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
async function syncSkillsToV1(skills, cfg, logger) {
|
|
2814
|
+
for (const skill of skills) {
|
|
2815
|
+
try {
|
|
2816
|
+
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
|
|
2817
|
+
name: skill.name,
|
|
2818
|
+
description: skill.description
|
|
2819
|
+
});
|
|
2820
|
+
} catch (err) {
|
|
2821
|
+
logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
var lastKnownRoster = [];
|
|
2826
|
+
function getLastKnownRoster() {
|
|
2827
|
+
return lastKnownRoster;
|
|
2828
|
+
}
|
|
2829
|
+
async function reconcileRoster(openClawAgents, cfg, logger) {
|
|
2830
|
+
const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
2831
|
+
const cohortAgents = data?.data ?? [];
|
|
2832
|
+
const cohortByName = new Map(cohortAgents.map((a) => [a.name.toLowerCase(), a]));
|
|
2833
|
+
const openClawNames = new Set(
|
|
2834
|
+
openClawAgents.map((a) => {
|
|
2835
|
+
const nameMap = cfg.agentNameMap;
|
|
2836
|
+
return (nameMap?.[a.id] ?? a.identity?.name ?? a.id).toLowerCase();
|
|
2837
|
+
})
|
|
2838
|
+
);
|
|
2839
|
+
for (const oc of openClawAgents) {
|
|
2840
|
+
const agentName = (cfg.agentNameMap?.[oc.id] ?? oc.identity?.name ?? oc.id).toLowerCase();
|
|
2841
|
+
const existing = cohortByName.get(agentName);
|
|
2842
|
+
if (!existing) {
|
|
2843
|
+
try {
|
|
2844
|
+
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/agents", {
|
|
2845
|
+
name: agentName,
|
|
2846
|
+
displayName: oc.identity?.name ?? agentName,
|
|
2847
|
+
emoji: oc.identity?.emoji ?? "\u{1F916}",
|
|
2848
|
+
model: oc.model,
|
|
2849
|
+
status: "idle"
|
|
2850
|
+
});
|
|
2851
|
+
logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
logger.warn(`cohort-sync: failed to provision agent "${agentName}": ${String(err)}`);
|
|
2854
|
+
}
|
|
2855
|
+
} else {
|
|
2856
|
+
const updates = {
|
|
2857
|
+
model: oc.model,
|
|
2858
|
+
status: "idle"
|
|
2859
|
+
};
|
|
2860
|
+
if (oc.identity?.name) {
|
|
2861
|
+
updates.displayName = oc.identity.name;
|
|
2862
|
+
}
|
|
2863
|
+
if (oc.identity?.emoji) {
|
|
2864
|
+
updates.emoji = oc.identity.emoji;
|
|
2865
|
+
}
|
|
2866
|
+
try {
|
|
2867
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${existing.id}`, updates);
|
|
2868
|
+
} catch (err) {
|
|
2869
|
+
logger.warn(`cohort-sync: failed to update agent "${agentName}": ${String(err)}`);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
for (const cohort of cohortAgents) {
|
|
2874
|
+
if (!openClawNames.has(cohort.name.toLowerCase())) {
|
|
2875
|
+
if (cohort.status === "archived" || cohort.status === "deleted" || cohort.status === "unreachable") {
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
try {
|
|
2879
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${cohort.id}`, {
|
|
2880
|
+
status: "unreachable"
|
|
2881
|
+
});
|
|
2882
|
+
logger.info(`cohort-sync: marked agent "${cohort.name}" as unreachable`);
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
logger.warn(
|
|
2885
|
+
`cohort-sync: failed to mark agent "${cohort.name}" as unreachable: ${String(err)}`
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
const updatedData = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
2891
|
+
const finalRoster = updatedData?.data ?? cohortAgents;
|
|
2892
|
+
lastKnownRoster = finalRoster;
|
|
2893
|
+
return finalRoster;
|
|
2894
|
+
}
|
|
2895
|
+
async function markAllUnreachable(cfg, logger) {
|
|
2896
|
+
const roster = getLastKnownRoster();
|
|
2897
|
+
if (roster.length === 0) {
|
|
2898
|
+
logger.warn("cohort-sync: no cached roster \u2014 skipping markAllUnreachable");
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
for (const agent of roster) {
|
|
2902
|
+
if (agent.status === "unreachable" || agent.status === "archived" || agent.status === "deleted") {
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
try {
|
|
2906
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
2907
|
+
status: "unreachable"
|
|
2908
|
+
});
|
|
2909
|
+
} catch (err) {
|
|
2910
|
+
logger.warn(`cohort-sync: failed to mark "${agent.name}" as unreachable: ${String(err)}`);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
logger.info("cohort-sync: all agents marked unreachable");
|
|
2914
|
+
}
|
|
2915
|
+
async function fullSync(agentName, model, cfg, logger, openClawAgents) {
|
|
2916
|
+
logger.info("cohort-sync: full sync starting");
|
|
2917
|
+
if (openClawAgents && openClawAgents.length > 0) {
|
|
2918
|
+
try {
|
|
2919
|
+
await reconcileRoster(openClawAgents, cfg, logger);
|
|
2920
|
+
} catch (err) {
|
|
2921
|
+
logger.warn(`cohort-sync: roster reconciliation failed: ${String(err)}`);
|
|
2922
|
+
}
|
|
2923
|
+
} else {
|
|
2924
|
+
await syncAgentStatus(agentName, "working", model, cfg, logger);
|
|
2925
|
+
}
|
|
2926
|
+
const skills = fetchSkills(logger);
|
|
2927
|
+
if (skills.length > 0) {
|
|
2928
|
+
await syncSkillsToV1(skills, cfg, logger);
|
|
2929
|
+
}
|
|
2930
|
+
logger.info("cohort-sync: full sync complete");
|
|
2931
|
+
}
|
|
2702
2932
|
|
|
2703
2933
|
// ../../node_modules/.pnpm/convex@1.33.0_patch_hash=l43bztwr6e2lbmpd6ao6hmcg24_react@19.2.1/node_modules/convex/dist/esm/index.js
|
|
2704
2934
|
var version = "1.33.0";
|
|
@@ -7630,14 +7860,14 @@ var require_node_gyp_build = __commonJS({
|
|
|
7630
7860
|
"../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
7631
7861
|
var fs4 = __require("fs");
|
|
7632
7862
|
var path4 = __require("path");
|
|
7633
|
-
var
|
|
7863
|
+
var os4 = __require("os");
|
|
7634
7864
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
7635
7865
|
var vars = process.config && process.config.variables || {};
|
|
7636
7866
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
7637
7867
|
var abi = process.versions.modules;
|
|
7638
7868
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
7639
|
-
var arch = process.env.npm_config_arch ||
|
|
7640
|
-
var platform = process.env.npm_config_platform ||
|
|
7869
|
+
var arch = process.env.npm_config_arch || os4.arch();
|
|
7870
|
+
var platform = process.env.npm_config_platform || os4.platform();
|
|
7641
7871
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
7642
7872
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
7643
7873
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -11542,127 +11772,125 @@ function reverseResolveAgentName(cohortName, forwardMap) {
|
|
|
11542
11772
|
return cohortName;
|
|
11543
11773
|
}
|
|
11544
11774
|
|
|
11545
|
-
// src/
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
function buildNotificationMessage(n) {
|
|
11554
|
-
let header;
|
|
11555
|
-
let cta;
|
|
11556
|
-
switch (n.type) {
|
|
11557
|
-
case "comment":
|
|
11558
|
-
if (n.isMentioned) {
|
|
11559
|
-
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
11560
|
-
By: ${n.actorName}`;
|
|
11561
|
-
cta = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
|
|
11562
|
-
} else {
|
|
11563
|
-
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
11564
|
-
From: ${n.actorName}`;
|
|
11565
|
-
cta = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
|
|
11566
|
-
}
|
|
11567
|
-
break;
|
|
11568
|
-
case "assignment":
|
|
11569
|
-
header = `You were assigned to task #${n.taskNumber} "${n.taskTitle}"
|
|
11570
|
-
By: ${n.actorName}`;
|
|
11571
|
-
cta = "Review the task description and begin working on it.";
|
|
11572
|
-
break;
|
|
11573
|
-
case "status_change":
|
|
11574
|
-
header = `Status changed on task #${n.taskNumber} "${n.taskTitle}"
|
|
11575
|
-
By: ${n.actorName}`;
|
|
11576
|
-
cta = "Review the status change and take any follow-up action needed.";
|
|
11577
|
-
break;
|
|
11578
|
-
default:
|
|
11579
|
-
header = `Notification on task #${n.taskNumber} "${n.taskTitle}"
|
|
11580
|
-
From: ${n.actorName}`;
|
|
11581
|
-
cta = "Check the task and respond if needed.";
|
|
11582
|
-
}
|
|
11583
|
-
const body = n.preview ? `
|
|
11584
|
-
Comment: "${n.preview}"` : "";
|
|
11585
|
-
let scope = "";
|
|
11586
|
-
if (n.taskDescription && n.type === "comment") {
|
|
11587
|
-
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
11588
|
-
scope = `
|
|
11589
|
-
|
|
11590
|
-
Scope: ${truncated}`;
|
|
11591
|
-
}
|
|
11592
|
-
const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
|
|
11593
|
-
|
|
11594
|
-
${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
11595
|
-
const promptBlock = n.type === "comment" ? `
|
|
11596
|
-
|
|
11597
|
-
---
|
|
11598
|
-
${prompt}` : "";
|
|
11599
|
-
return `${header}${scope}${body}
|
|
11600
|
-
|
|
11601
|
-
${cta}${promptBlock}`;
|
|
11602
|
-
}
|
|
11603
|
-
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
11604
|
-
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
11605
|
-
method: "POST",
|
|
11606
|
-
headers: {
|
|
11607
|
-
"Content-Type": "application/json",
|
|
11608
|
-
"Authorization": `Bearer ${hooksToken}`
|
|
11609
|
-
},
|
|
11610
|
-
body: JSON.stringify({
|
|
11611
|
-
message: buildNotificationMessage(n),
|
|
11612
|
-
name: "Cohort",
|
|
11613
|
-
agentId,
|
|
11614
|
-
deliver: false,
|
|
11615
|
-
sessionKey: `hook:cohort:task-${n.taskNumber}`
|
|
11616
|
-
})
|
|
11617
|
-
});
|
|
11618
|
-
if (!response.ok) {
|
|
11619
|
-
throw new Error(`/hooks/agent returned ${response.status} ${response.statusText}`);
|
|
11775
|
+
// src/commands.ts
|
|
11776
|
+
var cronRunNowPoll = null;
|
|
11777
|
+
async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
11778
|
+
if (cmd.type === "restart") {
|
|
11779
|
+
logger.info("cohort-sync: restart command, terminating in 500ms");
|
|
11780
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
11781
|
+
process.kill(process.pid, "SIGTERM");
|
|
11782
|
+
return;
|
|
11620
11783
|
}
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11784
|
+
if (cmd.type.startsWith("cron")) {
|
|
11785
|
+
if (!gwClient || !gwClient.isAlive()) {
|
|
11786
|
+
logger.warn(`cohort-sync: no gateway client, cannot execute ${cmd.type}`);
|
|
11787
|
+
return;
|
|
11788
|
+
}
|
|
11789
|
+
const nameMap = cfg.agentNameMap ?? {};
|
|
11790
|
+
switch (cmd.type) {
|
|
11791
|
+
case "cronEnable":
|
|
11792
|
+
await gwClient.request("cron.update", {
|
|
11793
|
+
jobId: cmd.payload?.jobId,
|
|
11794
|
+
patch: { enabled: true }
|
|
11795
|
+
});
|
|
11796
|
+
break;
|
|
11797
|
+
case "cronDisable":
|
|
11798
|
+
await gwClient.request("cron.update", {
|
|
11799
|
+
jobId: cmd.payload?.jobId,
|
|
11800
|
+
patch: { enabled: false }
|
|
11801
|
+
});
|
|
11802
|
+
break;
|
|
11803
|
+
case "cronDelete":
|
|
11804
|
+
await gwClient.request("cron.remove", {
|
|
11805
|
+
jobId: cmd.payload?.jobId
|
|
11806
|
+
});
|
|
11807
|
+
break;
|
|
11808
|
+
case "cronRunNow": {
|
|
11809
|
+
const runResult = await gwClient.request(
|
|
11810
|
+
"cron.run",
|
|
11811
|
+
{ jobId: cmd.payload?.jobId }
|
|
11812
|
+
);
|
|
11813
|
+
if (runResult?.ok && runResult?.ran) {
|
|
11814
|
+
const jobId = cmd.payload?.jobId;
|
|
11815
|
+
let polls = 0;
|
|
11816
|
+
if (cronRunNowPoll) clearInterval(cronRunNowPoll);
|
|
11817
|
+
const pollInterval = setInterval(async () => {
|
|
11818
|
+
polls++;
|
|
11819
|
+
if (polls >= 15) {
|
|
11820
|
+
clearInterval(pollInterval);
|
|
11821
|
+
cronRunNowPoll = null;
|
|
11822
|
+
return;
|
|
11823
|
+
}
|
|
11824
|
+
try {
|
|
11825
|
+
if (!gwClient || !gwClient.isAlive()) {
|
|
11826
|
+
clearInterval(pollInterval);
|
|
11827
|
+
cronRunNowPoll = null;
|
|
11828
|
+
return;
|
|
11829
|
+
}
|
|
11830
|
+
const pollResult = await gwClient.request("cron.list");
|
|
11831
|
+
const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
|
|
11832
|
+
const job = freshJobs.find((j) => j.id === jobId);
|
|
11833
|
+
if (job && !job.state?.runningAtMs) {
|
|
11834
|
+
clearInterval(pollInterval);
|
|
11835
|
+
cronRunNowPoll = null;
|
|
11836
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11837
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11838
|
+
}
|
|
11839
|
+
} catch {
|
|
11840
|
+
}
|
|
11841
|
+
}, 2e3);
|
|
11842
|
+
cronRunNowPoll = pollInterval;
|
|
11843
|
+
}
|
|
11844
|
+
break;
|
|
11845
|
+
}
|
|
11846
|
+
case "cronCreate": {
|
|
11847
|
+
const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
|
|
11848
|
+
await gwClient.request("cron.add", {
|
|
11849
|
+
job: {
|
|
11850
|
+
agentId,
|
|
11851
|
+
name: cmd.payload?.name,
|
|
11852
|
+
enabled: true,
|
|
11853
|
+
schedule: cmd.payload?.schedule,
|
|
11854
|
+
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11855
|
+
sessionTarget: "isolated",
|
|
11856
|
+
wakeMode: "now"
|
|
11857
|
+
}
|
|
11858
|
+
});
|
|
11859
|
+
break;
|
|
11860
|
+
}
|
|
11861
|
+
case "cronUpdate": {
|
|
11862
|
+
const patch = {};
|
|
11863
|
+
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11864
|
+
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11865
|
+
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11866
|
+
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11867
|
+
await gwClient.request("cron.update", {
|
|
11868
|
+
jobId: cmd.payload?.jobId,
|
|
11869
|
+
patch
|
|
11870
|
+
});
|
|
11871
|
+
break;
|
|
11872
|
+
}
|
|
11873
|
+
default:
|
|
11874
|
+
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11875
|
+
}
|
|
11876
|
+
if (gwClient.isAlive()) {
|
|
11877
|
+
try {
|
|
11878
|
+
const snapResult = await gwClient.request("cron.list");
|
|
11879
|
+
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11880
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11881
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11882
|
+
} catch (snapErr) {
|
|
11883
|
+
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11886
|
+
return;
|
|
11650
11887
|
}
|
|
11651
|
-
|
|
11652
|
-
if (!state.intervals) state.intervals = { heartbeat: null, activityFlush: null };
|
|
11653
|
-
if (!state.channelAgentBridge) state.channelAgentBridge = {};
|
|
11654
|
-
if (!state.unsubscribers) state.unsubscribers = [];
|
|
11655
|
-
if (!state.lastKnownRoster) state.lastKnownRoster = [];
|
|
11656
|
-
return state;
|
|
11657
|
-
}
|
|
11658
|
-
function clearHotState() {
|
|
11659
|
-
delete globalThis[HOT_KEY];
|
|
11660
|
-
}
|
|
11661
|
-
function setRosterHotState(roster) {
|
|
11662
|
-
getHotState().lastKnownRoster = roster;
|
|
11888
|
+
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
11663
11889
|
}
|
|
11664
|
-
|
|
11665
|
-
|
|
11890
|
+
|
|
11891
|
+
// src/convex-bridge.ts
|
|
11892
|
+
function deriveConvexUrl(apiUrl) {
|
|
11893
|
+
return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
|
|
11666
11894
|
}
|
|
11667
11895
|
var savedLogger = null;
|
|
11668
11896
|
function setLogger(logger) {
|
|
@@ -11678,266 +11906,18 @@ function getLogger() {
|
|
|
11678
11906
|
var client = null;
|
|
11679
11907
|
var savedConvexUrl = null;
|
|
11680
11908
|
var unsubscribers = [];
|
|
11681
|
-
function
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
getHotState().convexUrl = url;
|
|
11685
|
-
}
|
|
11686
|
-
function restoreFromHotReload(logger) {
|
|
11687
|
-
const state = getHotState();
|
|
11688
|
-
if (!client && state.client) {
|
|
11689
|
-
client = state.client;
|
|
11690
|
-
savedConvexUrl = state.convexUrl;
|
|
11691
|
-
logger.info("cohort-sync: recovered ConvexClient after hot-reload");
|
|
11692
|
-
}
|
|
11693
|
-
if (unsubscribers.length === 0 && state.unsubscribers.length > 0) {
|
|
11694
|
-
unsubscribers.push(...state.unsubscribers);
|
|
11695
|
-
logger.info(`cohort-sync: recovered ${state.unsubscribers.length} notification subscriptions after hot-reload`);
|
|
11696
|
-
}
|
|
11697
|
-
}
|
|
11698
|
-
function getOrCreateClient() {
|
|
11699
|
-
if (client) return client;
|
|
11700
|
-
const state = getHotState();
|
|
11701
|
-
if (state.client) {
|
|
11702
|
-
client = state.client;
|
|
11703
|
-
getLogger().info("cohort-sync: recovered ConvexClient from globalThis");
|
|
11704
|
-
return client;
|
|
11705
|
-
}
|
|
11706
|
-
if (!savedConvexUrl) return null;
|
|
11707
|
-
client = new ConvexClient(savedConvexUrl);
|
|
11708
|
-
getLogger().info(`cohort-sync: created fresh ConvexClient (${savedConvexUrl})`);
|
|
11709
|
-
state.client = client;
|
|
11710
|
-
state.convexUrl = savedConvexUrl;
|
|
11711
|
-
return client;
|
|
11712
|
-
}
|
|
11713
|
-
async function initSubscription(port, cfg, hooksToken, logger) {
|
|
11714
|
-
const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
|
|
11715
|
-
savedConvexUrl = convexUrl;
|
|
11716
|
-
const state = getHotState();
|
|
11717
|
-
if (state.client) {
|
|
11718
|
-
client = state.client;
|
|
11719
|
-
logger.info("cohort-sync: reusing hot-reload ConvexClient for subscription");
|
|
11720
|
-
} else {
|
|
11721
|
-
client = new ConvexClient(convexUrl);
|
|
11722
|
-
logger.info(`cohort-sync: created new ConvexClient for subscription (${convexUrl})`);
|
|
11909
|
+
function createClient(convexUrl) {
|
|
11910
|
+
if (client) {
|
|
11911
|
+
client.close();
|
|
11723
11912
|
}
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
logger.warn(
|
|
11728
|
-
`cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
|
|
11729
|
-
);
|
|
11730
|
-
return;
|
|
11731
|
-
}
|
|
11732
|
-
const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
|
|
11733
|
-
const reverseNameMap = {};
|
|
11734
|
-
if (cfg.agentNameMap) {
|
|
11735
|
-
for (const [openclawId, cohortName] of Object.entries(cfg.agentNameMap)) {
|
|
11736
|
-
reverseNameMap[cohortName] = openclawId;
|
|
11737
|
-
}
|
|
11738
|
-
}
|
|
11739
|
-
for (const agentName of agentNames) {
|
|
11740
|
-
const openclawAgentId = reverseNameMap[agentName] ?? agentName;
|
|
11741
|
-
logger.info(`cohort-sync: subscribing to notifications for agent "${agentName}" (openclawId: "${openclawAgentId}")`);
|
|
11742
|
-
let processing = false;
|
|
11743
|
-
const unsubscribe = client.onUpdate(
|
|
11744
|
-
getUndeliveredForPlugin,
|
|
11745
|
-
{ agent: agentName, apiKey: cfg.apiKey },
|
|
11746
|
-
async (notifications) => {
|
|
11747
|
-
if (processing) return;
|
|
11748
|
-
processing = true;
|
|
11749
|
-
try {
|
|
11750
|
-
for (const n of notifications) {
|
|
11751
|
-
try {
|
|
11752
|
-
await injectNotification(port, hooksToken, n, openclawAgentId);
|
|
11753
|
-
logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
|
|
11754
|
-
await client.mutation(markDeliveredByPlugin, {
|
|
11755
|
-
notificationId: n._id,
|
|
11756
|
-
apiKey: cfg.apiKey
|
|
11757
|
-
});
|
|
11758
|
-
} catch (err) {
|
|
11759
|
-
logger.warn(`cohort-sync: failed to inject notification ${n._id}: ${String(err)}`);
|
|
11760
|
-
}
|
|
11761
|
-
}
|
|
11762
|
-
} finally {
|
|
11763
|
-
processing = false;
|
|
11764
|
-
}
|
|
11765
|
-
},
|
|
11766
|
-
(err) => {
|
|
11767
|
-
logger.error(`cohort-sync: subscription error for "${agentName}": ${String(err)}`);
|
|
11768
|
-
}
|
|
11769
|
-
);
|
|
11770
|
-
unsubscribers.push(unsubscribe);
|
|
11771
|
-
}
|
|
11772
|
-
state.unsubscribers = [...unsubscribers];
|
|
11773
|
-
}
|
|
11774
|
-
function initCommandSubscription(cfg, logger, resolveAgentName) {
|
|
11775
|
-
const c = getOrCreateClient();
|
|
11776
|
-
if (!c) {
|
|
11777
|
-
logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
|
|
11778
|
-
return null;
|
|
11779
|
-
}
|
|
11780
|
-
let processing = false;
|
|
11781
|
-
const unsubscribe = c.onUpdate(
|
|
11782
|
-
getPendingCommandsForPlugin,
|
|
11783
|
-
{ apiKey: cfg.apiKey },
|
|
11784
|
-
async (commands) => {
|
|
11785
|
-
if (processing) return;
|
|
11786
|
-
if (commands.length === 0) return;
|
|
11787
|
-
processing = true;
|
|
11788
|
-
try {
|
|
11789
|
-
for (const cmd of commands) {
|
|
11790
|
-
logger.info(`cohort-sync: processing command: ${cmd.type} (${cmd._id})`);
|
|
11791
|
-
try {
|
|
11792
|
-
await c.mutation(acknowledgeCommandRef, {
|
|
11793
|
-
commandId: cmd._id,
|
|
11794
|
-
apiKey: cfg.apiKey
|
|
11795
|
-
});
|
|
11796
|
-
if (cmd.type === "restart") {
|
|
11797
|
-
logger.info("cohort-sync: restart acknowledged, terminating in 500ms");
|
|
11798
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
11799
|
-
process.kill(process.pid, "SIGTERM");
|
|
11800
|
-
return;
|
|
11801
|
-
}
|
|
11802
|
-
if (cmd.type.startsWith("cron")) {
|
|
11803
|
-
const hotState = getHotState();
|
|
11804
|
-
const port = hotState.gatewayPort;
|
|
11805
|
-
const token = hotState.gatewayToken;
|
|
11806
|
-
if (!port || !token) {
|
|
11807
|
-
logger.warn(`cohort-sync: no gateway port/token, cannot execute ${cmd.type}`);
|
|
11808
|
-
continue;
|
|
11809
|
-
}
|
|
11810
|
-
const gwClient = new GatewayClient(port, token, logger);
|
|
11811
|
-
try {
|
|
11812
|
-
await gwClient.connect();
|
|
11813
|
-
const nameMap = cfg.agentNameMap ?? {};
|
|
11814
|
-
switch (cmd.type) {
|
|
11815
|
-
case "cronEnable":
|
|
11816
|
-
await gwClient.request("cron.update", {
|
|
11817
|
-
jobId: cmd.payload?.jobId,
|
|
11818
|
-
patch: { enabled: true }
|
|
11819
|
-
});
|
|
11820
|
-
break;
|
|
11821
|
-
case "cronDisable":
|
|
11822
|
-
await gwClient.request("cron.update", {
|
|
11823
|
-
jobId: cmd.payload?.jobId,
|
|
11824
|
-
patch: { enabled: false }
|
|
11825
|
-
});
|
|
11826
|
-
break;
|
|
11827
|
-
case "cronDelete":
|
|
11828
|
-
await gwClient.request("cron.remove", {
|
|
11829
|
-
jobId: cmd.payload?.jobId
|
|
11830
|
-
});
|
|
11831
|
-
break;
|
|
11832
|
-
case "cronRunNow": {
|
|
11833
|
-
const runResult = await gwClient.request(
|
|
11834
|
-
"cron.run",
|
|
11835
|
-
{ jobId: cmd.payload?.jobId }
|
|
11836
|
-
);
|
|
11837
|
-
if (runResult?.ok && runResult?.ran) {
|
|
11838
|
-
const jobId = cmd.payload?.jobId;
|
|
11839
|
-
let polls = 0;
|
|
11840
|
-
const pollInterval = setInterval(async () => {
|
|
11841
|
-
polls++;
|
|
11842
|
-
if (polls >= 15) {
|
|
11843
|
-
clearInterval(pollInterval);
|
|
11844
|
-
return;
|
|
11845
|
-
}
|
|
11846
|
-
try {
|
|
11847
|
-
const pollClient = getHotState().gatewayProtocolClient;
|
|
11848
|
-
if (!pollClient || !pollClient.isAlive()) {
|
|
11849
|
-
clearInterval(pollInterval);
|
|
11850
|
-
return;
|
|
11851
|
-
}
|
|
11852
|
-
const pollResult = await pollClient.request("cron.list");
|
|
11853
|
-
const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
|
|
11854
|
-
const job = freshJobs.find((j) => j.id === jobId);
|
|
11855
|
-
if (job && !job.state?.runningAtMs) {
|
|
11856
|
-
clearInterval(pollInterval);
|
|
11857
|
-
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11858
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11859
|
-
}
|
|
11860
|
-
} catch {
|
|
11861
|
-
}
|
|
11862
|
-
}, 2e3);
|
|
11863
|
-
}
|
|
11864
|
-
break;
|
|
11865
|
-
}
|
|
11866
|
-
case "cronCreate": {
|
|
11867
|
-
const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
|
|
11868
|
-
await gwClient.request("cron.add", {
|
|
11869
|
-
job: {
|
|
11870
|
-
agentId,
|
|
11871
|
-
name: cmd.payload?.name,
|
|
11872
|
-
enabled: true,
|
|
11873
|
-
schedule: cmd.payload?.schedule,
|
|
11874
|
-
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11875
|
-
sessionTarget: "isolated",
|
|
11876
|
-
wakeMode: "now"
|
|
11877
|
-
}
|
|
11878
|
-
});
|
|
11879
|
-
break;
|
|
11880
|
-
}
|
|
11881
|
-
case "cronUpdate": {
|
|
11882
|
-
const patch = {};
|
|
11883
|
-
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11884
|
-
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11885
|
-
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11886
|
-
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11887
|
-
await gwClient.request("cron.update", {
|
|
11888
|
-
jobId: cmd.payload?.jobId,
|
|
11889
|
-
patch
|
|
11890
|
-
});
|
|
11891
|
-
break;
|
|
11892
|
-
}
|
|
11893
|
-
default:
|
|
11894
|
-
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11895
|
-
}
|
|
11896
|
-
if (gwClient.isAlive()) {
|
|
11897
|
-
try {
|
|
11898
|
-
const snapResult = await gwClient.request("cron.list");
|
|
11899
|
-
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11900
|
-
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11901
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11902
|
-
} catch (snapErr) {
|
|
11903
|
-
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11904
|
-
}
|
|
11905
|
-
}
|
|
11906
|
-
} finally {
|
|
11907
|
-
gwClient.close();
|
|
11908
|
-
}
|
|
11909
|
-
} else {
|
|
11910
|
-
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
11911
|
-
}
|
|
11912
|
-
} catch (err) {
|
|
11913
|
-
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
11914
|
-
}
|
|
11915
|
-
}
|
|
11916
|
-
} finally {
|
|
11917
|
-
processing = false;
|
|
11918
|
-
}
|
|
11919
|
-
},
|
|
11920
|
-
(err) => {
|
|
11921
|
-
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
11922
|
-
}
|
|
11923
|
-
);
|
|
11924
|
-
logger.info("cohort-sync: command subscription active");
|
|
11925
|
-
return unsubscribe;
|
|
11913
|
+
savedConvexUrl = convexUrl;
|
|
11914
|
+
client = new ConvexClient(convexUrl);
|
|
11915
|
+
return client;
|
|
11926
11916
|
}
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
if (!c) {
|
|
11930
|
-
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
11931
|
-
}
|
|
11932
|
-
return await c.mutation(addCommentFromPluginRef, {
|
|
11933
|
-
apiKey,
|
|
11934
|
-
taskNumber: args.taskNumber,
|
|
11935
|
-
agentName: args.agentName,
|
|
11936
|
-
content: args.content,
|
|
11937
|
-
noReply: args.noReply
|
|
11938
|
-
});
|
|
11917
|
+
function getClient() {
|
|
11918
|
+
return client;
|
|
11939
11919
|
}
|
|
11940
|
-
function
|
|
11920
|
+
function closeBridge() {
|
|
11941
11921
|
for (const unsub of unsubscribers) {
|
|
11942
11922
|
try {
|
|
11943
11923
|
unsub();
|
|
@@ -11945,33 +11925,32 @@ function closeSubscription() {
|
|
|
11945
11925
|
}
|
|
11946
11926
|
}
|
|
11947
11927
|
unsubscribers.length = 0;
|
|
11948
|
-
|
|
11949
|
-
for (const unsub of state.unsubscribers) {
|
|
11928
|
+
if (commandUnsubscriber) {
|
|
11950
11929
|
try {
|
|
11951
|
-
|
|
11930
|
+
commandUnsubscriber();
|
|
11952
11931
|
} catch {
|
|
11953
11932
|
}
|
|
11933
|
+
commandUnsubscriber = null;
|
|
11954
11934
|
}
|
|
11955
|
-
if (
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
} catch {
|
|
11959
|
-
}
|
|
11960
|
-
state.commandSubscription = null;
|
|
11961
|
-
}
|
|
11962
|
-
if (state.gatewayProtocolClient) {
|
|
11963
|
-
try {
|
|
11964
|
-
state.gatewayProtocolClient.close();
|
|
11965
|
-
} catch {
|
|
11966
|
-
}
|
|
11967
|
-
state.gatewayProtocolClient = null;
|
|
11935
|
+
if (client) {
|
|
11936
|
+
client.close();
|
|
11937
|
+
client = null;
|
|
11968
11938
|
}
|
|
11969
|
-
|
|
11970
|
-
client = null;
|
|
11971
|
-
clearHotState();
|
|
11939
|
+
savedConvexUrl = null;
|
|
11972
11940
|
}
|
|
11941
|
+
var commandUnsubscriber = null;
|
|
11942
|
+
var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
|
|
11943
|
+
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
11944
|
+
var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
|
|
11945
|
+
var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
|
|
11946
|
+
var getUndeliveredForPlugin = makeFunctionReference("notifications:getUndeliveredForPlugin");
|
|
11947
|
+
var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredByPlugin");
|
|
11948
|
+
var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
|
|
11949
|
+
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11950
|
+
var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
|
|
11951
|
+
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
11973
11952
|
async function pushTelemetry(apiKey, data) {
|
|
11974
|
-
const c =
|
|
11953
|
+
const c = getClient();
|
|
11975
11954
|
if (!c) return;
|
|
11976
11955
|
try {
|
|
11977
11956
|
await c.mutation(upsertTelemetryFromPlugin, { apiKey, ...data });
|
|
@@ -11980,7 +11959,7 @@ async function pushTelemetry(apiKey, data) {
|
|
|
11980
11959
|
}
|
|
11981
11960
|
}
|
|
11982
11961
|
async function pushSessions(apiKey, agentName, sessions) {
|
|
11983
|
-
const c =
|
|
11962
|
+
const c = getClient();
|
|
11984
11963
|
if (!c) return;
|
|
11985
11964
|
try {
|
|
11986
11965
|
await c.mutation(upsertSessionsFromPlugin, { apiKey, agentName, sessions });
|
|
@@ -11990,7 +11969,7 @@ async function pushSessions(apiKey, agentName, sessions) {
|
|
|
11990
11969
|
}
|
|
11991
11970
|
async function pushActivity(apiKey, entries) {
|
|
11992
11971
|
if (entries.length === 0) return;
|
|
11993
|
-
const c =
|
|
11972
|
+
const c = getClient();
|
|
11994
11973
|
if (!c) return;
|
|
11995
11974
|
try {
|
|
11996
11975
|
await c.mutation(pushActivityFromPluginRef, { apiKey, entries });
|
|
@@ -11999,271 +11978,203 @@ async function pushActivity(apiKey, entries) {
|
|
|
11999
11978
|
}
|
|
12000
11979
|
}
|
|
12001
11980
|
async function pushCronSnapshot(apiKey, jobs) {
|
|
12002
|
-
const c =
|
|
12003
|
-
if (!c) return;
|
|
11981
|
+
const c = getClient();
|
|
11982
|
+
if (!c) return false;
|
|
12004
11983
|
try {
|
|
12005
11984
|
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKey, jobs });
|
|
11985
|
+
return true;
|
|
12006
11986
|
} catch (err) {
|
|
12007
11987
|
getLogger().error(`cohort-sync: pushCronSnapshot failed: ${err}`);
|
|
11988
|
+
return false;
|
|
12008
11989
|
}
|
|
12009
11990
|
}
|
|
12010
|
-
function
|
|
12011
|
-
const
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
function clearIntervalsFromHot() {
|
|
12015
|
-
const state = getHotState();
|
|
12016
|
-
if (state.intervals.heartbeat) clearInterval(state.intervals.heartbeat);
|
|
12017
|
-
if (state.intervals.activityFlush) clearInterval(state.intervals.activityFlush);
|
|
12018
|
-
state.intervals = { heartbeat: null, activityFlush: null };
|
|
12019
|
-
}
|
|
12020
|
-
function addActivityToHot(entry) {
|
|
12021
|
-
const state = getHotState();
|
|
12022
|
-
state.activityBuffer.push(entry);
|
|
12023
|
-
const logger = savedLogger;
|
|
12024
|
-
if (logger) {
|
|
12025
|
-
logger.info(`cohort-sync: +activity [${entry.category}] "${entry.text}"`);
|
|
12026
|
-
}
|
|
12027
|
-
}
|
|
12028
|
-
function drainActivityFromHot() {
|
|
12029
|
-
const state = getHotState();
|
|
12030
|
-
const buf = state.activityBuffer;
|
|
12031
|
-
state.activityBuffer = [];
|
|
12032
|
-
return buf;
|
|
12033
|
-
}
|
|
12034
|
-
function setChannelAgent(channelId, agentName) {
|
|
12035
|
-
getHotState().channelAgentBridge[channelId] = agentName;
|
|
12036
|
-
}
|
|
12037
|
-
function getChannelAgent(channelId) {
|
|
12038
|
-
return getHotState().channelAgentBridge[channelId] ?? null;
|
|
12039
|
-
}
|
|
12040
|
-
|
|
12041
|
-
// src/sync.ts
|
|
12042
|
-
function extractJson(raw) {
|
|
12043
|
-
const jsonStart = raw.search(/[\[{]/);
|
|
12044
|
-
const jsonEndBracket = raw.lastIndexOf("]");
|
|
12045
|
-
const jsonEndBrace = raw.lastIndexOf("}");
|
|
12046
|
-
const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
|
|
12047
|
-
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
|
|
12048
|
-
throw new Error("No JSON found in output");
|
|
12049
|
-
}
|
|
12050
|
-
return raw.slice(jsonStart, jsonEnd + 1);
|
|
12051
|
-
}
|
|
12052
|
-
function fetchSkills(logger) {
|
|
12053
|
-
try {
|
|
12054
|
-
const raw = execSync("openclaw skills list --json", {
|
|
12055
|
-
encoding: "utf8",
|
|
12056
|
-
timeout: 3e4,
|
|
12057
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
12058
|
-
env: { ...process.env, NO_COLOR: "1" }
|
|
12059
|
-
});
|
|
12060
|
-
const parsed = JSON.parse(extractJson(raw));
|
|
12061
|
-
const list = Array.isArray(parsed) ? parsed : parsed?.skills ?? [];
|
|
12062
|
-
return list.map((s) => ({
|
|
12063
|
-
name: String(s.name ?? s.id ?? "unknown"),
|
|
12064
|
-
description: String(s.description ?? ""),
|
|
12065
|
-
source: String(s.source ?? s.origin ?? "unknown"),
|
|
12066
|
-
...s.emoji ? { emoji: String(s.emoji) } : {}
|
|
12067
|
-
}));
|
|
12068
|
-
} catch (err) {
|
|
12069
|
-
logger.warn(`cohort-sync: failed to fetch skills: ${String(err)}`);
|
|
12070
|
-
return [];
|
|
11991
|
+
async function callAddCommentFromPlugin(apiKey, args) {
|
|
11992
|
+
const c = getClient();
|
|
11993
|
+
if (!c) {
|
|
11994
|
+
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
12071
11995
|
}
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12079
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
12080
|
-
signal: AbortSignal.timeout(1e4)
|
|
12081
|
-
});
|
|
12082
|
-
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
12083
|
-
return res.json();
|
|
12084
|
-
}
|
|
12085
|
-
async function v1Patch(apiUrl, apiKey, path4, body) {
|
|
12086
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12087
|
-
method: "PATCH",
|
|
12088
|
-
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12089
|
-
body: JSON.stringify(body),
|
|
12090
|
-
signal: AbortSignal.timeout(1e4)
|
|
11996
|
+
return await c.mutation(addCommentFromPluginRef, {
|
|
11997
|
+
apiKey,
|
|
11998
|
+
taskNumber: args.taskNumber,
|
|
11999
|
+
agentName: args.agentName,
|
|
12000
|
+
content: args.content,
|
|
12001
|
+
noReply: args.noReply
|
|
12091
12002
|
});
|
|
12092
|
-
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
12093
12003
|
}
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
const available = agents.map((a) => a.name).join(", ") || "(none)";
|
|
12128
|
-
logger.warn(
|
|
12129
|
-
`cohort-sync: agent "${agentName}" not found in workspace \u2014 skipping status sync. Available agents: [${available}]. Configure agentNameMap in your plugin config to map OpenClaw agent IDs to Cohort names.`
|
|
12130
|
-
);
|
|
12131
|
-
return;
|
|
12132
|
-
}
|
|
12133
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
12134
|
-
status: normalizeStatus(status),
|
|
12135
|
-
model
|
|
12136
|
-
});
|
|
12137
|
-
logger.info(`cohort-sync: agent "${agentName}" \u2192 ${normalizeStatus(status)}`);
|
|
12138
|
-
} catch (err) {
|
|
12139
|
-
logger.warn(`cohort-sync: syncAgentStatus failed: ${String(err)}`);
|
|
12140
|
-
}
|
|
12141
|
-
}
|
|
12142
|
-
async function syncSkillsToV1(skills, cfg, logger) {
|
|
12143
|
-
for (const skill of skills) {
|
|
12144
|
-
try {
|
|
12145
|
-
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
|
|
12146
|
-
name: skill.name,
|
|
12147
|
-
description: skill.description
|
|
12148
|
-
});
|
|
12149
|
-
} catch (err) {
|
|
12150
|
-
logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
|
|
12151
|
-
}
|
|
12004
|
+
var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
|
|
12005
|
+
- Does your planned response address the task's stated scope? If not, do not comment.
|
|
12006
|
+
- Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
|
|
12007
|
+
- If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
|
|
12008
|
+
function buildNotificationMessage(n) {
|
|
12009
|
+
let header;
|
|
12010
|
+
let cta;
|
|
12011
|
+
switch (n.type) {
|
|
12012
|
+
case "comment":
|
|
12013
|
+
if (n.isMentioned) {
|
|
12014
|
+
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
12015
|
+
By: ${n.actorName}`;
|
|
12016
|
+
cta = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
|
|
12017
|
+
} else {
|
|
12018
|
+
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
12019
|
+
From: ${n.actorName}`;
|
|
12020
|
+
cta = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
|
|
12021
|
+
}
|
|
12022
|
+
break;
|
|
12023
|
+
case "assignment":
|
|
12024
|
+
header = `You were assigned to task #${n.taskNumber} "${n.taskTitle}"
|
|
12025
|
+
By: ${n.actorName}`;
|
|
12026
|
+
cta = "Review the task description and begin working on it.";
|
|
12027
|
+
break;
|
|
12028
|
+
case "status_change":
|
|
12029
|
+
header = `Status changed on task #${n.taskNumber} "${n.taskTitle}"
|
|
12030
|
+
By: ${n.actorName}`;
|
|
12031
|
+
cta = "Review the status change and take any follow-up action needed.";
|
|
12032
|
+
break;
|
|
12033
|
+
default:
|
|
12034
|
+
header = `Notification on task #${n.taskNumber} "${n.taskTitle}"
|
|
12035
|
+
From: ${n.actorName}`;
|
|
12036
|
+
cta = "Check the task and respond if needed.";
|
|
12152
12037
|
}
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
logger.info(`cohort-sync: recovered roster (${hotRoster.length} agents) after hot-reload`);
|
|
12038
|
+
const body = n.preview ? `
|
|
12039
|
+
Comment: "${n.preview}"` : "";
|
|
12040
|
+
let scope = "";
|
|
12041
|
+
if (n.taskDescription && n.type === "comment") {
|
|
12042
|
+
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
12043
|
+
scope = `
|
|
12044
|
+
|
|
12045
|
+
Scope: ${truncated}`;
|
|
12162
12046
|
}
|
|
12047
|
+
const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
|
|
12048
|
+
|
|
12049
|
+
${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
12050
|
+
const promptBlock = n.type === "comment" ? `
|
|
12051
|
+
|
|
12052
|
+
---
|
|
12053
|
+
${prompt}` : "";
|
|
12054
|
+
return `${header}${scope}${body}
|
|
12055
|
+
|
|
12056
|
+
${cta}${promptBlock}`;
|
|
12163
12057
|
}
|
|
12164
|
-
async function
|
|
12165
|
-
const
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
displayName: oc.identity?.name ?? agentName,
|
|
12182
|
-
emoji: oc.identity?.emoji ?? "\u{1F916}",
|
|
12183
|
-
model: oc.model,
|
|
12184
|
-
status: "idle"
|
|
12185
|
-
});
|
|
12186
|
-
logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
|
|
12187
|
-
} catch (err) {
|
|
12188
|
-
logger.warn(`cohort-sync: failed to provision agent "${agentName}": ${String(err)}`);
|
|
12189
|
-
}
|
|
12190
|
-
} else {
|
|
12191
|
-
const updates = {
|
|
12192
|
-
model: oc.model,
|
|
12193
|
-
status: "idle"
|
|
12194
|
-
};
|
|
12195
|
-
if (oc.identity?.name) {
|
|
12196
|
-
updates.displayName = oc.identity.name;
|
|
12197
|
-
}
|
|
12198
|
-
if (oc.identity?.emoji) {
|
|
12199
|
-
updates.emoji = oc.identity.emoji;
|
|
12200
|
-
}
|
|
12201
|
-
try {
|
|
12202
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${existing.id}`, updates);
|
|
12203
|
-
} catch (err) {
|
|
12204
|
-
logger.warn(`cohort-sync: failed to update agent "${agentName}": ${String(err)}`);
|
|
12205
|
-
}
|
|
12206
|
-
}
|
|
12207
|
-
}
|
|
12208
|
-
for (const cohort of cohortAgents) {
|
|
12209
|
-
if (!openClawNames.has(cohort.name.toLowerCase())) {
|
|
12210
|
-
if (cohort.status === "archived" || cohort.status === "deleted" || cohort.status === "unreachable") {
|
|
12211
|
-
continue;
|
|
12212
|
-
}
|
|
12213
|
-
try {
|
|
12214
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${cohort.id}`, {
|
|
12215
|
-
status: "unreachable"
|
|
12216
|
-
});
|
|
12217
|
-
logger.info(`cohort-sync: marked agent "${cohort.name}" as unreachable`);
|
|
12218
|
-
} catch (err) {
|
|
12219
|
-
logger.warn(
|
|
12220
|
-
`cohort-sync: failed to mark agent "${cohort.name}" as unreachable: ${String(err)}`
|
|
12221
|
-
);
|
|
12222
|
-
}
|
|
12223
|
-
}
|
|
12058
|
+
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
12059
|
+
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
12060
|
+
method: "POST",
|
|
12061
|
+
headers: {
|
|
12062
|
+
"Content-Type": "application/json",
|
|
12063
|
+
"Authorization": `Bearer ${hooksToken}`
|
|
12064
|
+
},
|
|
12065
|
+
body: JSON.stringify({
|
|
12066
|
+
message: buildNotificationMessage(n),
|
|
12067
|
+
name: "Cohort",
|
|
12068
|
+
agentId,
|
|
12069
|
+
deliver: false,
|
|
12070
|
+
sessionKey: `hook:cohort:task-${n.taskNumber}`
|
|
12071
|
+
})
|
|
12072
|
+
});
|
|
12073
|
+
if (!response.ok) {
|
|
12074
|
+
throw new Error(`/hooks/agent returned ${response.status} ${response.statusText}`);
|
|
12224
12075
|
}
|
|
12225
|
-
const updatedData = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
12226
|
-
const finalRoster = updatedData?.data ?? cohortAgents;
|
|
12227
|
-
lastKnownRoster = finalRoster;
|
|
12228
|
-
setRosterHotState(finalRoster);
|
|
12229
|
-
return finalRoster;
|
|
12230
12076
|
}
|
|
12231
|
-
async function
|
|
12232
|
-
const
|
|
12233
|
-
if (
|
|
12234
|
-
logger.warn("cohort-sync: no
|
|
12077
|
+
async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
12078
|
+
const c = getClient();
|
|
12079
|
+
if (!c) {
|
|
12080
|
+
logger.warn("cohort-sync: no ConvexClient \u2014 notification subscription skipped");
|
|
12235
12081
|
return;
|
|
12236
12082
|
}
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
12243
|
-
status: "unreachable"
|
|
12244
|
-
});
|
|
12245
|
-
} catch (err) {
|
|
12246
|
-
logger.warn(`cohort-sync: failed to mark "${agent.name}" as unreachable: ${String(err)}`);
|
|
12247
|
-
}
|
|
12083
|
+
if (!hooksToken) {
|
|
12084
|
+
logger.warn(
|
|
12085
|
+
`cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
|
|
12086
|
+
);
|
|
12087
|
+
return;
|
|
12248
12088
|
}
|
|
12249
|
-
|
|
12250
|
-
}
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
try {
|
|
12255
|
-
await reconcileRoster(openClawAgents, cfg, logger);
|
|
12256
|
-
} catch (err) {
|
|
12257
|
-
logger.warn(`cohort-sync: roster reconciliation failed: ${String(err)}`);
|
|
12089
|
+
const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
|
|
12090
|
+
const reverseNameMap = {};
|
|
12091
|
+
if (cfg.agentNameMap) {
|
|
12092
|
+
for (const [openclawId, cohortName] of Object.entries(cfg.agentNameMap)) {
|
|
12093
|
+
reverseNameMap[cohortName] = openclawId;
|
|
12258
12094
|
}
|
|
12259
|
-
} else {
|
|
12260
|
-
await syncAgentStatus(agentName, "working", model, cfg, logger);
|
|
12261
12095
|
}
|
|
12262
|
-
const
|
|
12263
|
-
|
|
12264
|
-
|
|
12096
|
+
for (const agentName of agentNames) {
|
|
12097
|
+
const openclawAgentId = reverseNameMap[agentName] ?? agentName;
|
|
12098
|
+
logger.info(`cohort-sync: subscribing to notifications for agent "${agentName}" (openclawId: "${openclawAgentId}")`);
|
|
12099
|
+
let processing = false;
|
|
12100
|
+
const unsubscribe = c.onUpdate(
|
|
12101
|
+
getUndeliveredForPlugin,
|
|
12102
|
+
{ agent: agentName, apiKey: cfg.apiKey },
|
|
12103
|
+
async (notifications) => {
|
|
12104
|
+
if (processing) return;
|
|
12105
|
+
processing = true;
|
|
12106
|
+
try {
|
|
12107
|
+
for (const n of notifications) {
|
|
12108
|
+
try {
|
|
12109
|
+
await injectNotification(port, hooksToken, n, openclawAgentId);
|
|
12110
|
+
logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
|
|
12111
|
+
await c.mutation(markDeliveredByPlugin, {
|
|
12112
|
+
notificationId: n._id,
|
|
12113
|
+
apiKey: cfg.apiKey
|
|
12114
|
+
});
|
|
12115
|
+
} catch (err) {
|
|
12116
|
+
logger.warn(`cohort-sync: failed to inject notification ${n._id}: ${String(err)}`);
|
|
12117
|
+
}
|
|
12118
|
+
}
|
|
12119
|
+
} finally {
|
|
12120
|
+
processing = false;
|
|
12121
|
+
}
|
|
12122
|
+
},
|
|
12123
|
+
(err) => {
|
|
12124
|
+
logger.error(`cohort-sync: subscription error for "${agentName}": ${String(err)}`);
|
|
12125
|
+
}
|
|
12126
|
+
);
|
|
12127
|
+
unsubscribers.push(unsubscribe);
|
|
12265
12128
|
}
|
|
12266
|
-
|
|
12129
|
+
}
|
|
12130
|
+
function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
12131
|
+
const c = getClient();
|
|
12132
|
+
if (!c) {
|
|
12133
|
+
logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
|
|
12134
|
+
return null;
|
|
12135
|
+
}
|
|
12136
|
+
let processing = false;
|
|
12137
|
+
const unsubscribe = c.onUpdate(
|
|
12138
|
+
getPendingCommandsForPlugin,
|
|
12139
|
+
{ apiKey: cfg.apiKey },
|
|
12140
|
+
async (commands) => {
|
|
12141
|
+
if (processing) return;
|
|
12142
|
+
if (commands.length === 0) return;
|
|
12143
|
+
processing = true;
|
|
12144
|
+
try {
|
|
12145
|
+
for (const cmd of commands) {
|
|
12146
|
+
logger.info(`cohort-sync: processing command: ${cmd.type} (${cmd._id})`);
|
|
12147
|
+
try {
|
|
12148
|
+
await c.mutation(acknowledgeCommandRef, {
|
|
12149
|
+
commandId: cmd._id,
|
|
12150
|
+
apiKey: cfg.apiKey
|
|
12151
|
+
});
|
|
12152
|
+
await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
|
|
12153
|
+
if (cmd.type === "restart") return;
|
|
12154
|
+
} catch (err) {
|
|
12155
|
+
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
12156
|
+
try {
|
|
12157
|
+
await c.mutation(failCommandRef, {
|
|
12158
|
+
commandId: cmd._id,
|
|
12159
|
+
apiKey: cfg.apiKey,
|
|
12160
|
+
reason: String(err).slice(0, 500)
|
|
12161
|
+
});
|
|
12162
|
+
} catch (failErr) {
|
|
12163
|
+
logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
|
|
12164
|
+
}
|
|
12165
|
+
}
|
|
12166
|
+
}
|
|
12167
|
+
} finally {
|
|
12168
|
+
processing = false;
|
|
12169
|
+
}
|
|
12170
|
+
},
|
|
12171
|
+
(err) => {
|
|
12172
|
+
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
12173
|
+
}
|
|
12174
|
+
);
|
|
12175
|
+
commandUnsubscriber = unsubscribe;
|
|
12176
|
+
logger.info("cohort-sync: command subscription active");
|
|
12177
|
+
return unsubscribe;
|
|
12267
12178
|
}
|
|
12268
12179
|
|
|
12269
12180
|
// src/gateway-client.ts
|
|
@@ -12450,13 +12361,7 @@ function parseHelloOk(response) {
|
|
|
12450
12361
|
snapshot: payload.snapshot
|
|
12451
12362
|
};
|
|
12452
12363
|
}
|
|
12453
|
-
|
|
12454
|
-
const g = globalThis;
|
|
12455
|
-
const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
|
|
12456
|
-
if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
|
|
12457
|
-
return hot.pendingGatewayRequests;
|
|
12458
|
-
}
|
|
12459
|
-
var GatewayClient2 = class {
|
|
12364
|
+
var GatewayClient = class {
|
|
12460
12365
|
port;
|
|
12461
12366
|
logger;
|
|
12462
12367
|
ws = null;
|
|
@@ -12470,9 +12375,12 @@ var GatewayClient2 = class {
|
|
|
12470
12375
|
tickIntervalMs = 15e3;
|
|
12471
12376
|
// default; overwritten by hello-ok response
|
|
12472
12377
|
deviceIdentity;
|
|
12378
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
12473
12379
|
/** Public Sets populated from hello-ok — consumers can inspect gateway capabilities */
|
|
12474
12380
|
availableMethods = /* @__PURE__ */ new Set();
|
|
12475
12381
|
availableEvents = /* @__PURE__ */ new Set();
|
|
12382
|
+
/** Called after a successful reconnection (WebSocket re-established after drop) */
|
|
12383
|
+
onReconnect = null;
|
|
12476
12384
|
/**
|
|
12477
12385
|
* @param port - Gateway WebSocket port
|
|
12478
12386
|
* @param token - Auth token (stored in closure via constructor param, NOT on this or globalThis)
|
|
@@ -12599,12 +12507,11 @@ var GatewayClient2 = class {
|
|
|
12599
12507
|
this.stopTickWatchdog();
|
|
12600
12508
|
diag("GW_CLIENT_WS_CLOSED", { port: this.port });
|
|
12601
12509
|
this.logger.warn("cohort-sync: gateway client WebSocket closed");
|
|
12602
|
-
const
|
|
12603
|
-
for (const [, entry] of pending) {
|
|
12510
|
+
for (const [, entry] of this.pendingRequests) {
|
|
12604
12511
|
clearTimeout(entry.timer);
|
|
12605
12512
|
entry.reject(new Error("Gateway WebSocket closed"));
|
|
12606
12513
|
}
|
|
12607
|
-
|
|
12514
|
+
this.pendingRequests.clear();
|
|
12608
12515
|
if (!settled) {
|
|
12609
12516
|
settle(new Error("Gateway WebSocket closed during handshake"));
|
|
12610
12517
|
}
|
|
@@ -12635,6 +12542,18 @@ var GatewayClient2 = class {
|
|
|
12635
12542
|
}
|
|
12636
12543
|
handlers.add(handler);
|
|
12637
12544
|
}
|
|
12545
|
+
/**
|
|
12546
|
+
* Remove an event handler previously registered with on().
|
|
12547
|
+
*/
|
|
12548
|
+
off(event, handler) {
|
|
12549
|
+
const handlers = this.eventHandlers.get(event);
|
|
12550
|
+
if (handlers) {
|
|
12551
|
+
handlers.delete(handler);
|
|
12552
|
+
if (handlers.size === 0) {
|
|
12553
|
+
this.eventHandlers.delete(event);
|
|
12554
|
+
}
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12638
12557
|
/**
|
|
12639
12558
|
* Send a request to the gateway and wait for the response.
|
|
12640
12559
|
*
|
|
@@ -12655,13 +12574,12 @@ var GatewayClient2 = class {
|
|
|
12655
12574
|
method,
|
|
12656
12575
|
params
|
|
12657
12576
|
};
|
|
12658
|
-
const pending = getPendingRequests();
|
|
12659
12577
|
return new Promise((resolve, reject) => {
|
|
12660
12578
|
const timer = setTimeout(() => {
|
|
12661
|
-
|
|
12579
|
+
this.pendingRequests.delete(id);
|
|
12662
12580
|
reject(new Error(`Gateway method "${method}" timed out after ${timeoutMs}ms`));
|
|
12663
12581
|
}, timeoutMs);
|
|
12664
|
-
|
|
12582
|
+
this.pendingRequests.set(id, {
|
|
12665
12583
|
resolve,
|
|
12666
12584
|
reject,
|
|
12667
12585
|
timer
|
|
@@ -12680,12 +12598,11 @@ var GatewayClient2 = class {
|
|
|
12680
12598
|
clearTimeout(this.reconnectTimer);
|
|
12681
12599
|
this.reconnectTimer = null;
|
|
12682
12600
|
}
|
|
12683
|
-
const
|
|
12684
|
-
for (const [, entry] of pending) {
|
|
12601
|
+
for (const [, entry] of this.pendingRequests) {
|
|
12685
12602
|
clearTimeout(entry.timer);
|
|
12686
12603
|
entry.reject(new Error("Gateway client closed"));
|
|
12687
12604
|
}
|
|
12688
|
-
|
|
12605
|
+
this.pendingRequests.clear();
|
|
12689
12606
|
if (this.ws) {
|
|
12690
12607
|
this.ws.close();
|
|
12691
12608
|
this.ws = null;
|
|
@@ -12715,10 +12632,9 @@ var GatewayClient2 = class {
|
|
|
12715
12632
|
}
|
|
12716
12633
|
}
|
|
12717
12634
|
handleResponse(frame) {
|
|
12718
|
-
const
|
|
12719
|
-
const entry = pending.get(frame.id);
|
|
12635
|
+
const entry = this.pendingRequests.get(frame.id);
|
|
12720
12636
|
if (!entry) return;
|
|
12721
|
-
|
|
12637
|
+
this.pendingRequests.delete(frame.id);
|
|
12722
12638
|
clearTimeout(entry.timer);
|
|
12723
12639
|
if (frame.ok) {
|
|
12724
12640
|
entry.resolve(frame.payload);
|
|
@@ -12793,6 +12709,7 @@ var GatewayClient2 = class {
|
|
|
12793
12709
|
diag("GW_CLIENT_RECONNECTING", { attempt: this.reconnectAttempts });
|
|
12794
12710
|
await this.connect();
|
|
12795
12711
|
diag("GW_CLIENT_RECONNECTED", { attempt: this.reconnectAttempts });
|
|
12712
|
+
this.onReconnect?.();
|
|
12796
12713
|
} catch (err) {
|
|
12797
12714
|
diag("GW_CLIENT_RECONNECT_FAILED", {
|
|
12798
12715
|
attempt: this.reconnectAttempts,
|
|
@@ -12803,6 +12720,94 @@ var GatewayClient2 = class {
|
|
|
12803
12720
|
}
|
|
12804
12721
|
};
|
|
12805
12722
|
|
|
12723
|
+
// src/micro-batch.ts
|
|
12724
|
+
var MicroBatch = class {
|
|
12725
|
+
buffer = [];
|
|
12726
|
+
timer = null;
|
|
12727
|
+
destroyed = false;
|
|
12728
|
+
maxSize;
|
|
12729
|
+
maxDelayMs;
|
|
12730
|
+
flushFn;
|
|
12731
|
+
onError;
|
|
12732
|
+
constructor(options) {
|
|
12733
|
+
this.maxSize = options.maxSize;
|
|
12734
|
+
this.maxDelayMs = options.maxDelayMs;
|
|
12735
|
+
this.flushFn = options.flush;
|
|
12736
|
+
this.onError = options.onError ?? ((err) => console.error("MicroBatch flush error:", err));
|
|
12737
|
+
}
|
|
12738
|
+
/**
|
|
12739
|
+
* Add an item to the batch.
|
|
12740
|
+
*
|
|
12741
|
+
* If the buffer was empty before this add, schedules an immediate flush
|
|
12742
|
+
* on the next tick (setTimeout 0). If the buffer already had items (burst),
|
|
12743
|
+
* starts a coalescing timer that fires after maxDelayMs.
|
|
12744
|
+
*
|
|
12745
|
+
* If the buffer reaches maxSize, flushes immediately.
|
|
12746
|
+
*/
|
|
12747
|
+
add(item) {
|
|
12748
|
+
if (this.destroyed) return;
|
|
12749
|
+
const wasEmpty = this.buffer.length === 0;
|
|
12750
|
+
this.buffer.push(item);
|
|
12751
|
+
if (this.buffer.length >= this.maxSize) {
|
|
12752
|
+
this.clearTimer();
|
|
12753
|
+
this.doFlush();
|
|
12754
|
+
return;
|
|
12755
|
+
}
|
|
12756
|
+
if (wasEmpty) {
|
|
12757
|
+
this.clearTimer();
|
|
12758
|
+
this.timer = setTimeout(() => {
|
|
12759
|
+
this.timer = null;
|
|
12760
|
+
this.doFlush();
|
|
12761
|
+
}, 0);
|
|
12762
|
+
} else if (!this.timer) {
|
|
12763
|
+
this.timer = setTimeout(() => {
|
|
12764
|
+
this.timer = null;
|
|
12765
|
+
this.doFlush();
|
|
12766
|
+
}, this.maxDelayMs);
|
|
12767
|
+
}
|
|
12768
|
+
}
|
|
12769
|
+
/**
|
|
12770
|
+
* Flush all remaining items. Used for graceful shutdown.
|
|
12771
|
+
* No-op when buffer is empty.
|
|
12772
|
+
*/
|
|
12773
|
+
drain() {
|
|
12774
|
+
this.clearTimer();
|
|
12775
|
+
if (this.buffer.length > 0) {
|
|
12776
|
+
this.doFlush();
|
|
12777
|
+
}
|
|
12778
|
+
}
|
|
12779
|
+
/**
|
|
12780
|
+
* Clear any pending timer and discard buffered items.
|
|
12781
|
+
* The batch will not accept new items after destroy.
|
|
12782
|
+
*/
|
|
12783
|
+
destroy() {
|
|
12784
|
+
this.destroyed = true;
|
|
12785
|
+
this.clearTimer();
|
|
12786
|
+
this.buffer = [];
|
|
12787
|
+
}
|
|
12788
|
+
doFlush() {
|
|
12789
|
+
if (this.buffer.length === 0) return;
|
|
12790
|
+
const items = this.buffer;
|
|
12791
|
+
this.buffer = [];
|
|
12792
|
+
try {
|
|
12793
|
+
const result = this.flushFn(items);
|
|
12794
|
+
if (result && typeof result.catch === "function") {
|
|
12795
|
+
result.catch((err) => {
|
|
12796
|
+
this.onError(err);
|
|
12797
|
+
});
|
|
12798
|
+
}
|
|
12799
|
+
} catch (err) {
|
|
12800
|
+
this.onError(err);
|
|
12801
|
+
}
|
|
12802
|
+
}
|
|
12803
|
+
clearTimer() {
|
|
12804
|
+
if (this.timer !== null) {
|
|
12805
|
+
clearTimeout(this.timer);
|
|
12806
|
+
this.timer = null;
|
|
12807
|
+
}
|
|
12808
|
+
}
|
|
12809
|
+
};
|
|
12810
|
+
|
|
12806
12811
|
// src/agent-state.ts
|
|
12807
12812
|
import { basename } from "node:path";
|
|
12808
12813
|
|
|
@@ -13051,6 +13056,16 @@ function buildActivityEntry(agentName, hook, context) {
|
|
|
13051
13056
|
return null;
|
|
13052
13057
|
}
|
|
13053
13058
|
}
|
|
13059
|
+
var channelAgentBridge = /* @__PURE__ */ new Map();
|
|
13060
|
+
function setChannelAgent(channelId, agentName) {
|
|
13061
|
+
channelAgentBridge.set(channelId, agentName);
|
|
13062
|
+
}
|
|
13063
|
+
function getChannelAgent(channelId) {
|
|
13064
|
+
return channelAgentBridge.get(channelId) ?? null;
|
|
13065
|
+
}
|
|
13066
|
+
function getChannelAgentBridge() {
|
|
13067
|
+
return channelAgentBridge;
|
|
13068
|
+
}
|
|
13054
13069
|
var AgentStateTracker = class {
|
|
13055
13070
|
agents = /* @__PURE__ */ new Map();
|
|
13056
13071
|
activityBuffer = [];
|
|
@@ -13301,11 +13316,26 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
|
13301
13316
|
`;
|
|
13302
13317
|
|
|
13303
13318
|
// src/hooks.ts
|
|
13304
|
-
var
|
|
13319
|
+
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
13320
|
+
"token",
|
|
13321
|
+
"apikey",
|
|
13322
|
+
"secret",
|
|
13323
|
+
"password",
|
|
13324
|
+
"credential",
|
|
13325
|
+
"authorization",
|
|
13326
|
+
"accesstoken",
|
|
13327
|
+
"refreshtoken",
|
|
13328
|
+
"bearer",
|
|
13329
|
+
"privatekey"
|
|
13330
|
+
]);
|
|
13305
13331
|
function dumpCtx(ctx) {
|
|
13306
13332
|
if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
|
|
13307
13333
|
const out = {};
|
|
13308
13334
|
for (const key of Object.keys(ctx)) {
|
|
13335
|
+
if (REDACT_KEYS.has(key.toLowerCase())) {
|
|
13336
|
+
out[key] = "[REDACTED]";
|
|
13337
|
+
continue;
|
|
13338
|
+
}
|
|
13309
13339
|
const val = ctx[key];
|
|
13310
13340
|
if (typeof val === "function") {
|
|
13311
13341
|
out[key] = "[Function]";
|
|
@@ -13331,8 +13361,7 @@ try {
|
|
|
13331
13361
|
PLUGIN_VERSION = pkgJson.version ?? "unknown";
|
|
13332
13362
|
} catch {
|
|
13333
13363
|
}
|
|
13334
|
-
diag("MODULE_LOADED", {
|
|
13335
|
-
var lastCronSnapshotJson = "";
|
|
13364
|
+
diag("MODULE_LOADED", { PLUGIN_VERSION });
|
|
13336
13365
|
function resolveGatewayToken(api) {
|
|
13337
13366
|
const rawToken = api.config?.gateway?.auth?.token;
|
|
13338
13367
|
if (typeof rawToken === "string") return rawToken;
|
|
@@ -13341,26 +13370,6 @@ function resolveGatewayToken(api) {
|
|
|
13341
13370
|
}
|
|
13342
13371
|
return null;
|
|
13343
13372
|
}
|
|
13344
|
-
async function quickCronSync(port, token, cfg, resolveAgentName, logger) {
|
|
13345
|
-
const client2 = new GatewayClient2(port, token, logger, PLUGIN_VERSION);
|
|
13346
|
-
try {
|
|
13347
|
-
await client2.connect();
|
|
13348
|
-
const result = await client2.request("cron.list", { includeDisabled: true });
|
|
13349
|
-
diag("GW_CLIENT_CRON_LIST_RESULT", { type: typeof result, isArray: Array.isArray(result), keys: result && typeof result === "object" ? Object.keys(result) : [], length: Array.isArray(result) ? result.length : void 0 });
|
|
13350
|
-
const jobs = Array.isArray(result) ? result : result?.jobs ?? [];
|
|
13351
|
-
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13352
|
-
const serialized = JSON.stringify(mapped);
|
|
13353
|
-
if (serialized !== lastCronSnapshotJson) {
|
|
13354
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13355
|
-
lastCronSnapshotJson = serialized;
|
|
13356
|
-
diag("HEARTBEAT_CRON_PUSHED", { count: mapped.length });
|
|
13357
|
-
} else {
|
|
13358
|
-
diag("HEARTBEAT_CRON_UNCHANGED", {});
|
|
13359
|
-
}
|
|
13360
|
-
} finally {
|
|
13361
|
-
client2.close();
|
|
13362
|
-
}
|
|
13363
|
-
}
|
|
13364
13373
|
function registerCronEventHandlers(client2, cfg, resolveAgentName) {
|
|
13365
13374
|
if (client2.availableEvents.has("cron")) {
|
|
13366
13375
|
let debounceTimer = null;
|
|
@@ -13412,26 +13421,25 @@ function resolveIdentity(configIdentity, workspaceDir) {
|
|
|
13412
13421
|
avatar: configIdentity?.avatar ?? fileIdentity?.avatar
|
|
13413
13422
|
};
|
|
13414
13423
|
}
|
|
13424
|
+
var tracker = null;
|
|
13415
13425
|
function getOrCreateTracker() {
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
var
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
);
|
|
13428
|
-
function saveSessionsToDisk(tracker) {
|
|
13426
|
+
if (!tracker) tracker = new AgentStateTracker();
|
|
13427
|
+
return tracker;
|
|
13428
|
+
}
|
|
13429
|
+
var gatewayPort = null;
|
|
13430
|
+
var gatewayToken = null;
|
|
13431
|
+
var persistentGwClient = null;
|
|
13432
|
+
var gwClientInitialized = false;
|
|
13433
|
+
var keepaliveInterval = null;
|
|
13434
|
+
var commandUnsubscriber2 = null;
|
|
13435
|
+
var STATE_FILE_PATH = "";
|
|
13436
|
+
function saveSessionsToDisk(tracker2) {
|
|
13429
13437
|
try {
|
|
13430
|
-
const state =
|
|
13438
|
+
const state = tracker2.exportState();
|
|
13431
13439
|
const data = {
|
|
13432
13440
|
sessions: [],
|
|
13433
13441
|
sessionKeyToAgent: Object.fromEntries(state.sessionKeyToAgent),
|
|
13434
|
-
channelAgents:
|
|
13442
|
+
channelAgents: Object.fromEntries(getChannelAgentBridge()),
|
|
13435
13443
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13436
13444
|
};
|
|
13437
13445
|
for (const [name, agent] of state.agents) {
|
|
@@ -13443,7 +13451,7 @@ function saveSessionsToDisk(tracker) {
|
|
|
13443
13451
|
} catch {
|
|
13444
13452
|
}
|
|
13445
13453
|
}
|
|
13446
|
-
function loadSessionsFromDisk(
|
|
13454
|
+
function loadSessionsFromDisk(tracker2, logger) {
|
|
13447
13455
|
try {
|
|
13448
13456
|
if (!fs3.existsSync(STATE_FILE_PATH)) return;
|
|
13449
13457
|
const data = JSON.parse(fs3.readFileSync(STATE_FILE_PATH, "utf8"));
|
|
@@ -13453,13 +13461,13 @@ function loadSessionsFromDisk(tracker, logger) {
|
|
|
13453
13461
|
}
|
|
13454
13462
|
let count = 0;
|
|
13455
13463
|
for (const { agentName, key } of data.sessions) {
|
|
13456
|
-
if (!
|
|
13457
|
-
|
|
13464
|
+
if (!tracker2.hasSession(agentName, key)) {
|
|
13465
|
+
tracker2.addSession(agentName, key);
|
|
13458
13466
|
count++;
|
|
13459
13467
|
}
|
|
13460
13468
|
}
|
|
13461
13469
|
for (const [key, agent] of Object.entries(data.sessionKeyToAgent)) {
|
|
13462
|
-
|
|
13470
|
+
tracker2.setSessionAgent(key, agent);
|
|
13463
13471
|
}
|
|
13464
13472
|
for (const [channelId, agent] of Object.entries(data.channelAgents ?? {})) {
|
|
13465
13473
|
setChannelAgent(channelId, agent);
|
|
@@ -13487,15 +13495,48 @@ async function fetchAgentContext(apiKey, apiUrl, logger) {
|
|
|
13487
13495
|
return POCKET_GUIDE;
|
|
13488
13496
|
}
|
|
13489
13497
|
}
|
|
13498
|
+
async function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
|
|
13499
|
+
const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
|
|
13500
|
+
await client2.connect();
|
|
13501
|
+
persistentGwClient = client2;
|
|
13502
|
+
gwClientInitialized = true;
|
|
13503
|
+
registerCronEventHandlers(client2, cfg, resolveAgentName);
|
|
13504
|
+
if (client2.availableEvents.has("shutdown")) {
|
|
13505
|
+
client2.on("shutdown", () => {
|
|
13506
|
+
diag("GW_CLIENT_SHUTDOWN_EVENT", {});
|
|
13507
|
+
logger.info("cohort-sync: gateway shutdown event received");
|
|
13508
|
+
});
|
|
13509
|
+
}
|
|
13510
|
+
client2.onReconnect = async () => {
|
|
13511
|
+
diag("GW_CLIENT_RECONNECTED_RESUBSCRIBE", {});
|
|
13512
|
+
registerCronEventHandlers(client2, cfg, resolveAgentName);
|
|
13513
|
+
try {
|
|
13514
|
+
const cronResult2 = await client2.request("cron.list", { includeDisabled: true });
|
|
13515
|
+
const jobs2 = Array.isArray(cronResult2) ? cronResult2 : cronResult2?.jobs ?? [];
|
|
13516
|
+
const mapped2 = jobs2.map((j) => mapCronJob(j, resolveAgentName));
|
|
13517
|
+
await pushCronSnapshot(cfg.apiKey, mapped2);
|
|
13518
|
+
diag("GW_CLIENT_RECONNECT_CRON_PUSH", { count: mapped2.length });
|
|
13519
|
+
} catch (err) {
|
|
13520
|
+
diag("GW_CLIENT_RECONNECT_CRON_FAILED", { error: String(err) });
|
|
13521
|
+
}
|
|
13522
|
+
};
|
|
13523
|
+
diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
|
|
13524
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13525
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13526
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13527
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13528
|
+
diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
|
|
13529
|
+
}
|
|
13490
13530
|
function registerHooks(api, cfg) {
|
|
13531
|
+
STATE_FILE_PATH = path3.join(cfg.stateDir, "session-state.json");
|
|
13491
13532
|
const { logger, config } = api;
|
|
13492
13533
|
const nameMap = cfg.agentNameMap;
|
|
13493
|
-
const
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
logger
|
|
13534
|
+
const tracker2 = getOrCreateTracker();
|
|
13535
|
+
const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
|
|
13536
|
+
createClient(convexUrl);
|
|
13537
|
+
setLogger(logger);
|
|
13538
|
+
logger.info(`cohort-sync: registerHooks v${PLUGIN_VERSION}`);
|
|
13497
13539
|
diag("REGISTER_HOOKS", {
|
|
13498
|
-
BUILD_ID,
|
|
13499
13540
|
PLUGIN_VERSION,
|
|
13500
13541
|
hasNameMap: !!nameMap,
|
|
13501
13542
|
nameMapKeys: nameMap ? Object.keys(nameMap) : [],
|
|
@@ -13504,10 +13545,6 @@ function registerHooks(api, cfg) {
|
|
|
13504
13545
|
agentIds: (config?.agents?.list ?? []).map((a) => a.id),
|
|
13505
13546
|
agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
|
|
13506
13547
|
});
|
|
13507
|
-
setConvexUrl(cfg);
|
|
13508
|
-
setLogger(logger);
|
|
13509
|
-
restoreFromHotReload(logger);
|
|
13510
|
-
restoreRosterFromHotReload(getRosterHotState(), logger);
|
|
13511
13548
|
const identityNameMap = {};
|
|
13512
13549
|
const mainIdentity = parseIdentityFile(process.cwd());
|
|
13513
13550
|
if (mainIdentity?.name) {
|
|
@@ -13520,14 +13557,14 @@ function registerHooks(api, cfg) {
|
|
|
13520
13557
|
}
|
|
13521
13558
|
}
|
|
13522
13559
|
diag("IDENTITY_NAME_MAP", { identityNameMap });
|
|
13523
|
-
if (
|
|
13524
|
-
loadSessionsFromDisk(
|
|
13525
|
-
const restoredAgents =
|
|
13560
|
+
if (tracker2.getAgentNames().length === 0) {
|
|
13561
|
+
loadSessionsFromDisk(tracker2, logger);
|
|
13562
|
+
const restoredAgents = tracker2.getAgentNames();
|
|
13526
13563
|
for (const agentName of restoredAgents) {
|
|
13527
|
-
const sessSnapshot =
|
|
13564
|
+
const sessSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
13528
13565
|
if (sessSnapshot.length > 0) {
|
|
13529
13566
|
pushSessions(cfg.apiKey, agentName, sessSnapshot).then(() => {
|
|
13530
|
-
|
|
13567
|
+
tracker2.markSessionsPushed(agentName);
|
|
13531
13568
|
logger.info(`cohort-sync: pushed ${sessSnapshot.length} restored sessions for ${agentName}`);
|
|
13532
13569
|
}).catch((err) => {
|
|
13533
13570
|
logger.warn(`cohort-sync: failed to push restored sessions for ${agentName}: ${String(err)}`);
|
|
@@ -13558,7 +13595,7 @@ function registerHooks(api, cfg) {
|
|
|
13558
13595
|
}
|
|
13559
13596
|
const sessionKey = ctx.sessionKey ?? ctx.sessionId;
|
|
13560
13597
|
if (sessionKey && typeof sessionKey === "string") {
|
|
13561
|
-
const mapped =
|
|
13598
|
+
const mapped = tracker2.getSessionAgent(sessionKey);
|
|
13562
13599
|
if (mapped) {
|
|
13563
13600
|
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "sessionKey_mapped", sessionKey, mapped });
|
|
13564
13601
|
return mapped;
|
|
@@ -13580,10 +13617,10 @@ function registerHooks(api, cfg) {
|
|
|
13580
13617
|
if (channelId && typeof channelId === "string") {
|
|
13581
13618
|
const channelAgent = getChannelAgent(channelId);
|
|
13582
13619
|
if (channelAgent) {
|
|
13583
|
-
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_bridge", channelId, channelAgent, bridgeState:
|
|
13620
|
+
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_bridge", channelId, channelAgent, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
|
|
13584
13621
|
return channelAgent;
|
|
13585
13622
|
}
|
|
13586
|
-
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_raw", channelId, bridgeState:
|
|
13623
|
+
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_raw", channelId, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
|
|
13587
13624
|
return String(channelId);
|
|
13588
13625
|
}
|
|
13589
13626
|
const resolved = resolveAgentName("main");
|
|
@@ -13604,47 +13641,20 @@ function registerHooks(api, cfg) {
|
|
|
13604
13641
|
}
|
|
13605
13642
|
return "unknown";
|
|
13606
13643
|
}
|
|
13607
|
-
const
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
})
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
clearIntervalsFromHot();
|
|
13615
|
-
heartbeatInterval = setInterval(() => {
|
|
13616
|
-
pushHeartbeat().catch((err) => {
|
|
13617
|
-
logger.warn(`cohort-sync: heartbeat tick failed: ${String(err)}`);
|
|
13618
|
-
});
|
|
13619
|
-
}, 12e4);
|
|
13620
|
-
activityFlushInterval = setInterval(() => {
|
|
13621
|
-
flushActivityBuffer().catch((err) => {
|
|
13622
|
-
logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
|
|
13623
|
-
});
|
|
13624
|
-
}, 3e3);
|
|
13625
|
-
saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
|
|
13626
|
-
logger.info("cohort-sync: intervals created (heartbeat=2m, activityFlush=3s)");
|
|
13627
|
-
{
|
|
13628
|
-
const hotState = getHotState();
|
|
13629
|
-
if (hotState.commandSubscription) {
|
|
13630
|
-
hotState.commandSubscription();
|
|
13631
|
-
hotState.commandSubscription = null;
|
|
13632
|
-
}
|
|
13633
|
-
const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
|
|
13634
|
-
hotState.commandSubscription = unsub;
|
|
13635
|
-
}
|
|
13644
|
+
const activityBatch = new MicroBatch({
|
|
13645
|
+
maxSize: 10,
|
|
13646
|
+
maxDelayMs: 1e3,
|
|
13647
|
+
flush: (entries) => pushActivity(cfg.apiKey, entries),
|
|
13648
|
+
onError: (err) => logger.warn(`cohort-sync: activity batch flush failed: ${String(err)}`)
|
|
13649
|
+
});
|
|
13650
|
+
const KEEPALIVE_INTERVAL_MS = 15e4;
|
|
13636
13651
|
{
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
const hotState = getHotState();
|
|
13641
|
-
hotState.gatewayPort = port;
|
|
13642
|
-
hotState.gatewayToken = token;
|
|
13643
|
-
diag("REGISTER_HOOKS_CRON_SYNC", { port });
|
|
13644
|
-
quickCronSync(port, token, cfg, resolveAgentName, logger).catch((err) => {
|
|
13645
|
-
diag("REGISTER_HOOKS_CRON_SYNC_FAILED", { error: String(err) });
|
|
13646
|
-
});
|
|
13652
|
+
if (commandUnsubscriber2) {
|
|
13653
|
+
commandUnsubscriber2();
|
|
13654
|
+
commandUnsubscriber2 = null;
|
|
13647
13655
|
}
|
|
13656
|
+
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13657
|
+
commandUnsubscriber2 = unsub;
|
|
13648
13658
|
}
|
|
13649
13659
|
api.registerTool((toolCtx) => {
|
|
13650
13660
|
const agentId = toolCtx.agentId ?? "main";
|
|
@@ -13760,52 +13770,6 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13760
13770
|
if (m.includes("deepseek")) return 128e3;
|
|
13761
13771
|
return 2e5;
|
|
13762
13772
|
}
|
|
13763
|
-
async function pushHeartbeat() {
|
|
13764
|
-
const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
|
|
13765
|
-
for (const agentId of allAgentIds) {
|
|
13766
|
-
const agentName = resolveAgentName(agentId);
|
|
13767
|
-
const pruned = tracker.pruneStaleSessions(agentName, 864e5);
|
|
13768
|
-
if (pruned.length > 0) {
|
|
13769
|
-
logger.info(`cohort-sync: pruned ${pruned.length} stale sessions for ${agentName}`);
|
|
13770
|
-
}
|
|
13771
|
-
}
|
|
13772
|
-
for (const agentId of allAgentIds) {
|
|
13773
|
-
const agentName = resolveAgentName(agentId);
|
|
13774
|
-
try {
|
|
13775
|
-
const snapshot = tracker.getTelemetrySnapshot(agentName);
|
|
13776
|
-
if (snapshot) {
|
|
13777
|
-
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13778
|
-
} else {
|
|
13779
|
-
logger.info(`cohort-sync: heartbeat skipped ${agentName} \u2014 no snapshot in tracker`);
|
|
13780
|
-
}
|
|
13781
|
-
} catch (err) {
|
|
13782
|
-
logger.warn(`cohort-sync: heartbeat push failed for ${agentName}: ${String(err)}`);
|
|
13783
|
-
}
|
|
13784
|
-
}
|
|
13785
|
-
for (const agentId of allAgentIds) {
|
|
13786
|
-
const agentName = resolveAgentName(agentId);
|
|
13787
|
-
try {
|
|
13788
|
-
if (tracker.shouldPushSessions(agentName)) {
|
|
13789
|
-
const sessSnapshot = tracker.getSessionsSnapshot(agentName);
|
|
13790
|
-
await pushSessions(cfg.apiKey, agentName, sessSnapshot);
|
|
13791
|
-
tracker.markSessionsPushed(agentName);
|
|
13792
|
-
}
|
|
13793
|
-
} catch (err) {
|
|
13794
|
-
logger.warn(`cohort-sync: heartbeat session push failed for ${agentName}: ${String(err)}`);
|
|
13795
|
-
}
|
|
13796
|
-
}
|
|
13797
|
-
saveSessionsToDisk(tracker);
|
|
13798
|
-
logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
|
|
13799
|
-
}
|
|
13800
|
-
async function flushActivityBuffer() {
|
|
13801
|
-
const entries = drainActivityFromHot();
|
|
13802
|
-
if (entries.length === 0) return;
|
|
13803
|
-
try {
|
|
13804
|
-
await pushActivity(cfg.apiKey, entries);
|
|
13805
|
-
} catch (err) {
|
|
13806
|
-
logger.warn(`cohort-sync: activity flush failed: ${String(err)}`);
|
|
13807
|
-
}
|
|
13808
|
-
}
|
|
13809
13773
|
api.on("gateway_start", async (event) => {
|
|
13810
13774
|
diag("HOOK_gateway_start", { port: event.port, eventKeys: Object.keys(event) });
|
|
13811
13775
|
try {
|
|
@@ -13831,24 +13795,20 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13831
13795
|
setChannelAgent(mp, agentName);
|
|
13832
13796
|
}
|
|
13833
13797
|
}
|
|
13834
|
-
diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge:
|
|
13835
|
-
|
|
13836
|
-
hotState.gatewayPort = event.port;
|
|
13798
|
+
diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: Object.fromEntries(getChannelAgentBridge()) });
|
|
13799
|
+
gatewayPort = event.port;
|
|
13837
13800
|
const token = resolveGatewayToken(api);
|
|
13838
13801
|
if (token) {
|
|
13802
|
+
gatewayToken = token;
|
|
13839
13803
|
diag("GW_CLIENT_CONNECTING", { port: event.port, hasToken: true });
|
|
13840
13804
|
try {
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
const
|
|
13847
|
-
|
|
13848
|
-
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13849
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13850
|
-
lastCronSnapshotJson = JSON.stringify(mapped);
|
|
13851
|
-
diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
|
|
13805
|
+
await initGatewayClient(event.port, token, cfg, resolveAgentName, logger);
|
|
13806
|
+
if (commandUnsubscriber2) {
|
|
13807
|
+
commandUnsubscriber2();
|
|
13808
|
+
commandUnsubscriber2 = null;
|
|
13809
|
+
}
|
|
13810
|
+
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13811
|
+
commandUnsubscriber2 = unsub;
|
|
13852
13812
|
} catch (err) {
|
|
13853
13813
|
diag("GW_CLIENT_CONNECT_FAILED", { error: String(err) });
|
|
13854
13814
|
logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
|
|
@@ -13857,7 +13817,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13857
13817
|
diag("GW_CLIENT_NO_TOKEN", {});
|
|
13858
13818
|
logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
|
|
13859
13819
|
}
|
|
13860
|
-
await
|
|
13820
|
+
await startNotificationSubscription(
|
|
13861
13821
|
event.port,
|
|
13862
13822
|
cfg,
|
|
13863
13823
|
api.config.hooks?.token,
|
|
@@ -13869,31 +13829,48 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13869
13829
|
for (const agentId of allAgentIds) {
|
|
13870
13830
|
const agentName = resolveAgentName(agentId);
|
|
13871
13831
|
try {
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
const snapshot =
|
|
13832
|
+
tracker2.setModel(agentName, resolveModel(agentId));
|
|
13833
|
+
tracker2.updateStatus(agentName, "idle");
|
|
13834
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13875
13835
|
if (snapshot) {
|
|
13876
13836
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13877
|
-
|
|
13837
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13878
13838
|
}
|
|
13879
13839
|
} catch (err) {
|
|
13880
13840
|
logger.warn(`cohort-sync: initial telemetry seed failed for ${agentName}: ${String(err)}`);
|
|
13881
13841
|
}
|
|
13882
13842
|
}
|
|
13883
13843
|
logger.info(`cohort-sync: seeded telemetry for ${allAgentIds.length} agents`);
|
|
13844
|
+
if (keepaliveInterval) clearInterval(keepaliveInterval);
|
|
13845
|
+
keepaliveInterval = setInterval(async () => {
|
|
13846
|
+
for (const agentId of allAgentIds) {
|
|
13847
|
+
const agentName = resolveAgentName(agentId);
|
|
13848
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13849
|
+
if (snapshot) {
|
|
13850
|
+
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION }).catch(() => {
|
|
13851
|
+
});
|
|
13852
|
+
}
|
|
13853
|
+
}
|
|
13854
|
+
for (const agentId of allAgentIds) {
|
|
13855
|
+
const agentName = resolveAgentName(agentId);
|
|
13856
|
+
tracker2.pruneStaleSessions(agentName, 864e5);
|
|
13857
|
+
}
|
|
13858
|
+
saveSessionsToDisk(tracker2);
|
|
13859
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
13860
|
+
logger.info(`cohort-sync: keepalive interval started (${KEEPALIVE_INTERVAL_MS / 1e3}s)`);
|
|
13884
13861
|
});
|
|
13885
13862
|
api.on("agent_end", async (event, ctx) => {
|
|
13886
13863
|
diag("HOOK_agent_end", { ctx: dumpCtx(ctx), success: event.success, error: event.error, durationMs: event.durationMs });
|
|
13887
13864
|
const agentId = ctx.agentId ?? "main";
|
|
13888
13865
|
const agentName = resolveAgentName(agentId);
|
|
13889
13866
|
try {
|
|
13890
|
-
|
|
13867
|
+
tracker2.updateStatus(agentName, "idle");
|
|
13891
13868
|
await syncAgentStatus(agentName, "idle", resolveModel(agentId), cfg, logger);
|
|
13892
|
-
if (
|
|
13893
|
-
const snapshot =
|
|
13869
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13870
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13894
13871
|
if (snapshot) {
|
|
13895
13872
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13896
|
-
|
|
13873
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13897
13874
|
}
|
|
13898
13875
|
}
|
|
13899
13876
|
if (event.success === false) {
|
|
@@ -13903,7 +13880,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13903
13880
|
durationMs: event.durationMs,
|
|
13904
13881
|
sessionKey: ctx.sessionKey
|
|
13905
13882
|
});
|
|
13906
|
-
if (entry)
|
|
13883
|
+
if (entry) activityBatch.add(entry);
|
|
13907
13884
|
}
|
|
13908
13885
|
} catch (err) {
|
|
13909
13886
|
logger.warn(`cohort-sync: agent_end sync failed: ${String(err)}`);
|
|
@@ -13928,31 +13905,31 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13928
13905
|
const agentName = resolveAgentName(agentId);
|
|
13929
13906
|
try {
|
|
13930
13907
|
const sessionKey = ctx.sessionKey;
|
|
13931
|
-
|
|
13908
|
+
tracker2.updateFromLlmOutput(agentName, sessionKey, {
|
|
13932
13909
|
model,
|
|
13933
13910
|
tokensIn: usage.input ?? 0,
|
|
13934
13911
|
tokensOut: usage.output ?? 0,
|
|
13935
13912
|
contextTokens,
|
|
13936
13913
|
contextLimit
|
|
13937
13914
|
});
|
|
13938
|
-
if (sessionKey && !
|
|
13939
|
-
|
|
13915
|
+
if (sessionKey && !tracker2.hasSession(agentName, sessionKey)) {
|
|
13916
|
+
tracker2.addSession(agentName, sessionKey);
|
|
13940
13917
|
logger.info(`cohort-sync: inferred session for ${agentName} from llm_output (${sessionKey})`);
|
|
13941
13918
|
}
|
|
13942
13919
|
if (sessionKey) {
|
|
13943
|
-
|
|
13920
|
+
tracker2.setSessionAgent(sessionKey, agentName);
|
|
13944
13921
|
}
|
|
13945
|
-
if (
|
|
13946
|
-
const snapshot =
|
|
13922
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13923
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13947
13924
|
if (snapshot) {
|
|
13948
13925
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13949
|
-
|
|
13926
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13950
13927
|
}
|
|
13951
13928
|
}
|
|
13952
|
-
if (
|
|
13953
|
-
const sessionsSnapshot =
|
|
13929
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
13930
|
+
const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
13954
13931
|
await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
|
|
13955
|
-
|
|
13932
|
+
tracker2.markSessionsPushed(agentName);
|
|
13956
13933
|
}
|
|
13957
13934
|
} catch (err) {
|
|
13958
13935
|
logger.warn(`cohort-sync: llm_output telemetry failed: ${String(err)}`);
|
|
@@ -13963,15 +13940,15 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13963
13940
|
const agentId = ctx.agentId ?? "main";
|
|
13964
13941
|
const agentName = resolveAgentName(agentId);
|
|
13965
13942
|
try {
|
|
13966
|
-
|
|
13943
|
+
tracker2.updateFromCompaction(agentName, {
|
|
13967
13944
|
contextTokens: event.tokenCount ?? 0,
|
|
13968
13945
|
contextLimit: getModelContextLimit(resolveModel(agentId))
|
|
13969
13946
|
});
|
|
13970
|
-
if (
|
|
13971
|
-
const snapshot =
|
|
13947
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13948
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13972
13949
|
if (snapshot) {
|
|
13973
13950
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13974
|
-
|
|
13951
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13975
13952
|
}
|
|
13976
13953
|
}
|
|
13977
13954
|
const entry = buildActivityEntry(agentName, "after_compaction", {
|
|
@@ -13979,7 +13956,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13979
13956
|
compactedCount: event.compactedCount,
|
|
13980
13957
|
sessionKey: ctx.sessionKey
|
|
13981
13958
|
});
|
|
13982
|
-
if (entry)
|
|
13959
|
+
if (entry) activityBatch.add(entry);
|
|
13983
13960
|
} catch (err) {
|
|
13984
13961
|
logger.warn(`cohort-sync: after_compaction telemetry failed: ${String(err)}`);
|
|
13985
13962
|
}
|
|
@@ -13990,26 +13967,39 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13990
13967
|
const agentName = resolveAgentName(agentId);
|
|
13991
13968
|
diag("HOOK_before_agent_start_RESOLVED", { agentId, agentName, ctxChannelId: ctx.channelId, ctxMessageProvider: ctx.messageProvider, ctxSessionKey: ctx.sessionKey, ctxAccountId: ctx.accountId });
|
|
13992
13969
|
try {
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13970
|
+
if (!gwClientInitialized && gatewayPort && gatewayToken) {
|
|
13971
|
+
try {
|
|
13972
|
+
await initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
|
|
13973
|
+
if (commandUnsubscriber2) {
|
|
13974
|
+
commandUnsubscriber2();
|
|
13975
|
+
commandUnsubscriber2 = null;
|
|
13976
|
+
}
|
|
13977
|
+
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13978
|
+
commandUnsubscriber2 = unsub;
|
|
13979
|
+
} catch (err) {
|
|
13980
|
+
diag("GW_CLIENT_LAZY_INIT_FAILED", { error: String(err) });
|
|
13981
|
+
}
|
|
13982
|
+
}
|
|
13983
|
+
tracker2.updateStatus(agentName, "working");
|
|
13984
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13985
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13996
13986
|
if (snapshot) {
|
|
13997
13987
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13998
|
-
|
|
13988
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13999
13989
|
}
|
|
14000
13990
|
}
|
|
14001
13991
|
const sessionKey = ctx.sessionKey;
|
|
14002
|
-
if (sessionKey && !
|
|
14003
|
-
|
|
14004
|
-
|
|
13992
|
+
if (sessionKey && !tracker2.hasSession(agentName, sessionKey)) {
|
|
13993
|
+
tracker2.addSession(agentName, sessionKey);
|
|
13994
|
+
tracker2.setSessionAgent(sessionKey, agentName);
|
|
14005
13995
|
logger.info(`cohort-sync: inferred session for ${agentName} (${sessionKey})`);
|
|
14006
|
-
if (
|
|
14007
|
-
const sessSnapshot =
|
|
13996
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
13997
|
+
const sessSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
14008
13998
|
await pushSessions(cfg.apiKey, agentName, sessSnapshot);
|
|
14009
|
-
|
|
13999
|
+
tracker2.markSessionsPushed(agentName);
|
|
14010
14000
|
}
|
|
14011
14001
|
} else if (sessionKey) {
|
|
14012
|
-
|
|
14002
|
+
tracker2.setSessionAgent(sessionKey, agentName);
|
|
14013
14003
|
}
|
|
14014
14004
|
const ctxChannelId = ctx.channelId;
|
|
14015
14005
|
if (ctxChannelId) {
|
|
@@ -14029,11 +14019,11 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14029
14019
|
const agentName = resolveAgentName(agentId);
|
|
14030
14020
|
try {
|
|
14031
14021
|
const sessionKey = ctx.sessionId ?? String(Date.now());
|
|
14032
|
-
|
|
14033
|
-
if (
|
|
14034
|
-
const sessionsSnapshot =
|
|
14022
|
+
tracker2.addSession(agentName, sessionKey);
|
|
14023
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
14024
|
+
const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
14035
14025
|
await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
|
|
14036
|
-
|
|
14026
|
+
tracker2.markSessionsPushed(agentName);
|
|
14037
14027
|
}
|
|
14038
14028
|
const parsed = parseSessionKey(sessionKey);
|
|
14039
14029
|
const entry = buildActivityEntry(agentName, "session_start", {
|
|
@@ -14041,7 +14031,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14041
14031
|
sessionKey,
|
|
14042
14032
|
resumedFrom: event.resumedFrom
|
|
14043
14033
|
});
|
|
14044
|
-
if (entry)
|
|
14034
|
+
if (entry) activityBatch.add(entry);
|
|
14045
14035
|
} catch (err) {
|
|
14046
14036
|
logger.warn(`cohort-sync: session_start tracking failed: ${String(err)}`);
|
|
14047
14037
|
}
|
|
@@ -14052,18 +14042,18 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14052
14042
|
const agentName = resolveAgentName(agentId);
|
|
14053
14043
|
try {
|
|
14054
14044
|
const sessionKey = ctx.sessionId ?? "";
|
|
14055
|
-
|
|
14056
|
-
if (
|
|
14057
|
-
const sessionsSnapshot =
|
|
14045
|
+
tracker2.removeSession(agentName, sessionKey);
|
|
14046
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
14047
|
+
const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
14058
14048
|
await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
|
|
14059
|
-
|
|
14049
|
+
tracker2.markSessionsPushed(agentName);
|
|
14060
14050
|
}
|
|
14061
14051
|
const entry = buildActivityEntry(agentName, "session_end", {
|
|
14062
14052
|
sessionKey,
|
|
14063
14053
|
messageCount: event.messageCount,
|
|
14064
14054
|
durationMs: event.durationMs
|
|
14065
14055
|
});
|
|
14066
|
-
if (entry)
|
|
14056
|
+
if (entry) activityBatch.add(entry);
|
|
14067
14057
|
} catch (err) {
|
|
14068
14058
|
logger.warn(`cohort-sync: session_end tracking failed: ${String(err)}`);
|
|
14069
14059
|
}
|
|
@@ -14080,7 +14070,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14080
14070
|
sessionKey: ctx.sessionKey,
|
|
14081
14071
|
model: resolveModel(ctx.agentId ?? "main")
|
|
14082
14072
|
});
|
|
14083
|
-
if (entry)
|
|
14073
|
+
if (entry) activityBatch.add(entry);
|
|
14084
14074
|
} catch (err) {
|
|
14085
14075
|
logger.warn(`cohort-sync: after_tool_call activity failed: ${String(err)}`);
|
|
14086
14076
|
}
|
|
@@ -14089,7 +14079,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14089
14079
|
diag("HOOK_message_received_RAW", {
|
|
14090
14080
|
ctx: dumpCtx(ctx),
|
|
14091
14081
|
event: dumpEvent(_event),
|
|
14092
|
-
bridgeStateBefore:
|
|
14082
|
+
bridgeStateBefore: Object.fromEntries(getChannelAgentBridge())
|
|
14093
14083
|
});
|
|
14094
14084
|
const agentName = resolveAgentFromContext(ctx);
|
|
14095
14085
|
const channel = ctx.channelId;
|
|
@@ -14104,7 +14094,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14104
14094
|
const entry = buildActivityEntry(agentName, "message_received", {
|
|
14105
14095
|
channel: channel ?? "unknown"
|
|
14106
14096
|
});
|
|
14107
|
-
if (entry)
|
|
14097
|
+
if (entry) activityBatch.add(entry);
|
|
14108
14098
|
} catch (err) {
|
|
14109
14099
|
logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
|
|
14110
14100
|
}
|
|
@@ -14113,7 +14103,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14113
14103
|
diag("HOOK_message_sent_RAW", {
|
|
14114
14104
|
ctx: dumpCtx(ctx),
|
|
14115
14105
|
event: dumpEvent(event),
|
|
14116
|
-
bridgeStateBefore:
|
|
14106
|
+
bridgeStateBefore: Object.fromEntries(getChannelAgentBridge())
|
|
14117
14107
|
});
|
|
14118
14108
|
const agentName = resolveAgentFromContext(ctx);
|
|
14119
14109
|
const channel = ctx.channelId;
|
|
@@ -14132,7 +14122,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14132
14122
|
success: event.success,
|
|
14133
14123
|
error: event.error
|
|
14134
14124
|
});
|
|
14135
|
-
if (entry)
|
|
14125
|
+
if (entry) activityBatch.add(entry);
|
|
14136
14126
|
} catch (err) {
|
|
14137
14127
|
logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
|
|
14138
14128
|
}
|
|
@@ -14145,7 +14135,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14145
14135
|
const entry = buildActivityEntry(agentName, "before_compaction", {
|
|
14146
14136
|
sessionKey: ctx.sessionKey
|
|
14147
14137
|
});
|
|
14148
|
-
if (entry)
|
|
14138
|
+
if (entry) activityBatch.add(entry);
|
|
14149
14139
|
} catch (err) {
|
|
14150
14140
|
logger.warn(`cohort-sync: before_compaction activity failed: ${String(err)}`);
|
|
14151
14141
|
}
|
|
@@ -14159,25 +14149,24 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14159
14149
|
reason: event.reason,
|
|
14160
14150
|
sessionKey: ctx.sessionKey
|
|
14161
14151
|
});
|
|
14162
|
-
if (entry)
|
|
14152
|
+
if (entry) activityBatch.add(entry);
|
|
14163
14153
|
} catch (err) {
|
|
14164
14154
|
logger.warn(`cohort-sync: before_reset activity failed: ${String(err)}`);
|
|
14165
14155
|
}
|
|
14166
14156
|
});
|
|
14167
14157
|
api.on("gateway_stop", async () => {
|
|
14168
|
-
diag("HOOK_gateway_stop", { bridgeState:
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
});
|
|
14158
|
+
diag("HOOK_gateway_stop", { bridgeState: Object.fromEntries(getChannelAgentBridge()) });
|
|
14159
|
+
if (keepaliveInterval) {
|
|
14160
|
+
clearInterval(keepaliveInterval);
|
|
14161
|
+
keepaliveInterval = null;
|
|
14162
|
+
}
|
|
14163
|
+
activityBatch.drain();
|
|
14175
14164
|
const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
|
|
14176
14165
|
for (const agentId of allAgentIds) {
|
|
14177
14166
|
const agentName = resolveAgentName(agentId);
|
|
14178
14167
|
try {
|
|
14179
|
-
|
|
14180
|
-
const snapshot =
|
|
14168
|
+
tracker2.updateStatus(agentName, "unreachable");
|
|
14169
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
14181
14170
|
if (snapshot) {
|
|
14182
14171
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
14183
14172
|
}
|
|
@@ -14185,15 +14174,20 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14185
14174
|
logger.warn(`cohort-sync: final unreachable push failed for ${agentName}: ${String(err)}`);
|
|
14186
14175
|
}
|
|
14187
14176
|
}
|
|
14188
|
-
saveSessionsToDisk(
|
|
14177
|
+
saveSessionsToDisk(tracker2);
|
|
14178
|
+
if (persistentGwClient) {
|
|
14179
|
+
persistentGwClient.close();
|
|
14180
|
+
persistentGwClient = null;
|
|
14181
|
+
gwClientInitialized = false;
|
|
14182
|
+
}
|
|
14189
14183
|
try {
|
|
14190
14184
|
await markAllUnreachable(cfg, logger);
|
|
14191
14185
|
} catch (err) {
|
|
14192
14186
|
logger.warn(`cohort-sync: markAllUnreachable failed: ${String(err)}`);
|
|
14193
14187
|
}
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
logger.info("cohort-sync:
|
|
14188
|
+
tracker2.clear();
|
|
14189
|
+
closeBridge();
|
|
14190
|
+
logger.info("cohort-sync: gateway stopped, all resources cleaned up");
|
|
14197
14191
|
});
|
|
14198
14192
|
}
|
|
14199
14193
|
|
|
@@ -14299,7 +14293,7 @@ function registerCohortCli(ctx, cfg) {
|
|
|
14299
14293
|
agents.push({ id, name });
|
|
14300
14294
|
}
|
|
14301
14295
|
} else {
|
|
14302
|
-
agents.push({ id: "main", name: "
|
|
14296
|
+
agents.push({ id: "main", name: "main" });
|
|
14303
14297
|
}
|
|
14304
14298
|
const manifest = { agents };
|
|
14305
14299
|
logger.info("cohort: Starting device authorization...");
|
|
@@ -14359,40 +14353,10 @@ function registerCohortCli(ctx, cfg) {
|
|
|
14359
14353
|
// index.ts
|
|
14360
14354
|
init_keychain();
|
|
14361
14355
|
var DEFAULT_API_URL = "https://fortunate-chipmunk-286.convex.site";
|
|
14362
|
-
async function doActivate(api) {
|
|
14363
|
-
const cfg = api.pluginConfig;
|
|
14364
|
-
const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
|
|
14365
|
-
api.logger.info(`cohort-sync: activating (api: ${apiUrl})`);
|
|
14366
|
-
let apiKey = cfg?.apiKey;
|
|
14367
|
-
if (!apiKey) {
|
|
14368
|
-
try {
|
|
14369
|
-
apiKey = await getCredential(apiUrl) ?? void 0;
|
|
14370
|
-
} catch (err) {
|
|
14371
|
-
api.logger.error(
|
|
14372
|
-
`cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
14373
|
-
);
|
|
14374
|
-
}
|
|
14375
|
-
}
|
|
14376
|
-
if (!apiKey) {
|
|
14377
|
-
api.logger.warn(
|
|
14378
|
-
"cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
|
|
14379
|
-
);
|
|
14380
|
-
return;
|
|
14381
|
-
}
|
|
14382
|
-
api.logger.info(`cohort-sync: activated (api: ${apiUrl})`);
|
|
14383
|
-
registerHooks(api, {
|
|
14384
|
-
apiUrl,
|
|
14385
|
-
apiKey,
|
|
14386
|
-
agentNameMap: cfg?.agentNameMap
|
|
14387
|
-
});
|
|
14388
|
-
}
|
|
14389
14356
|
var plugin = {
|
|
14390
14357
|
id: "cohort-sync",
|
|
14391
14358
|
name: "Cohort Sync",
|
|
14392
14359
|
description: "Syncs agent status and skills to Cohort dashboard",
|
|
14393
|
-
// register() is synchronous — the SDK does not await it.
|
|
14394
|
-
// We register CLI here, then self-activate async (the gateway does not
|
|
14395
|
-
// call activate() for extension-directory plugins).
|
|
14396
14360
|
register(api) {
|
|
14397
14361
|
const cfg = api.pluginConfig;
|
|
14398
14362
|
const apiUrl = cfg?.apiUrl ?? DEFAULT_API_URL;
|
|
@@ -14404,15 +14368,39 @@ var plugin = {
|
|
|
14404
14368
|
}),
|
|
14405
14369
|
{ commands: ["cohort"] }
|
|
14406
14370
|
);
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14371
|
+
api.registerService({
|
|
14372
|
+
id: "cohort-sync-core",
|
|
14373
|
+
async start(svcCtx) {
|
|
14374
|
+
api.logger.info(`cohort-sync: service starting (api: ${apiUrl})`);
|
|
14375
|
+
let apiKey = cfg?.apiKey;
|
|
14376
|
+
if (!apiKey) {
|
|
14377
|
+
try {
|
|
14378
|
+
apiKey = await getCredential(apiUrl) ?? void 0;
|
|
14379
|
+
} catch (err) {
|
|
14380
|
+
api.logger.error(
|
|
14381
|
+
`cohort-sync: keychain lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
14382
|
+
);
|
|
14383
|
+
}
|
|
14384
|
+
}
|
|
14385
|
+
if (!apiKey) {
|
|
14386
|
+
api.logger.warn(
|
|
14387
|
+
"cohort-sync: no API key found \u2014 run 'openclaw cohort auth' to authenticate"
|
|
14388
|
+
);
|
|
14389
|
+
return;
|
|
14390
|
+
}
|
|
14391
|
+
api.logger.info(`cohort-sync: activated (api: ${apiUrl})`);
|
|
14392
|
+
registerHooks(api, {
|
|
14393
|
+
apiUrl,
|
|
14394
|
+
apiKey,
|
|
14395
|
+
stateDir: svcCtx.stateDir,
|
|
14396
|
+
agentNameMap: cfg?.agentNameMap
|
|
14397
|
+
});
|
|
14398
|
+
},
|
|
14399
|
+
async stop() {
|
|
14400
|
+
closeBridge();
|
|
14401
|
+
api.logger.info("cohort-sync: service stopped");
|
|
14402
|
+
}
|
|
14403
|
+
});
|
|
14416
14404
|
}
|
|
14417
14405
|
};
|
|
14418
14406
|
var index_default = plugin;
|