@cfio/cohort-sync 0.7.0 → 0.8.1
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 +885 -928
- package/dist/openclaw.plugin.json +3 -3
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,9 +16,9 @@ __export(keychain_exports, {
|
|
|
16
16
|
setCredential: () => setCredential
|
|
17
17
|
});
|
|
18
18
|
import { execFile } from "node:child_process";
|
|
19
|
-
import
|
|
19
|
+
import os4 from "node:os";
|
|
20
20
|
function assertMacOS(operation) {
|
|
21
|
-
if (
|
|
21
|
+
if (os4.platform() !== "darwin") {
|
|
22
22
|
throw new Error(
|
|
23
23
|
`cohort-sync: ${operation} requires macOS Keychain. On Linux/Windows, set your API key in OpenClaw config: plugins.entries.cohort-sync.config.apiKey`
|
|
24
24
|
);
|
|
@@ -100,6 +100,7 @@ var init_keychain = __esm({
|
|
|
100
100
|
|
|
101
101
|
// src/hooks.ts
|
|
102
102
|
import fs3 from "node:fs";
|
|
103
|
+
import os3 from "node:os";
|
|
103
104
|
import path3 from "node:path";
|
|
104
105
|
|
|
105
106
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
@@ -2710,6 +2711,228 @@ var Type = type_exports2;
|
|
|
2710
2711
|
|
|
2711
2712
|
// src/sync.ts
|
|
2712
2713
|
import { execSync } from "node:child_process";
|
|
2714
|
+
function extractJson(raw) {
|
|
2715
|
+
const jsonStart = raw.search(/[\[{]/);
|
|
2716
|
+
const jsonEndBracket = raw.lastIndexOf("]");
|
|
2717
|
+
const jsonEndBrace = raw.lastIndexOf("}");
|
|
2718
|
+
const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
|
|
2719
|
+
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
|
|
2720
|
+
throw new Error("No JSON found in output");
|
|
2721
|
+
}
|
|
2722
|
+
return raw.slice(jsonStart, jsonEnd + 1);
|
|
2723
|
+
}
|
|
2724
|
+
function fetchSkills(logger) {
|
|
2725
|
+
try {
|
|
2726
|
+
const raw = execSync("openclaw skills list --json", {
|
|
2727
|
+
encoding: "utf8",
|
|
2728
|
+
timeout: 3e4,
|
|
2729
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2730
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
2731
|
+
});
|
|
2732
|
+
const parsed = JSON.parse(extractJson(raw));
|
|
2733
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.skills ?? [];
|
|
2734
|
+
return list.map((s) => ({
|
|
2735
|
+
name: String(s.name ?? s.id ?? "unknown"),
|
|
2736
|
+
description: String(s.description ?? ""),
|
|
2737
|
+
source: String(s.source ?? s.origin ?? "unknown"),
|
|
2738
|
+
...s.emoji ? { emoji: String(s.emoji) } : {}
|
|
2739
|
+
}));
|
|
2740
|
+
} catch (err) {
|
|
2741
|
+
logger.warn(`cohort-sync: failed to fetch skills: ${String(err)}`);
|
|
2742
|
+
return [];
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
2746
|
+
function normalizeStatus(status) {
|
|
2747
|
+
return VALID_STATUSES.has(status) ? status : "idle";
|
|
2748
|
+
}
|
|
2749
|
+
async function v1Get(apiUrl, apiKey, path4) {
|
|
2750
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
2751
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
2752
|
+
signal: AbortSignal.timeout(1e4)
|
|
2753
|
+
});
|
|
2754
|
+
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
2755
|
+
return res.json();
|
|
2756
|
+
}
|
|
2757
|
+
async function v1Patch(apiUrl, apiKey, path4, body) {
|
|
2758
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
2759
|
+
method: "PATCH",
|
|
2760
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
2761
|
+
body: JSON.stringify(body),
|
|
2762
|
+
signal: AbortSignal.timeout(1e4)
|
|
2763
|
+
});
|
|
2764
|
+
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
2765
|
+
}
|
|
2766
|
+
async function v1Post(apiUrl, apiKey, path4, body) {
|
|
2767
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
2768
|
+
method: "POST",
|
|
2769
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
2770
|
+
body: JSON.stringify(body),
|
|
2771
|
+
signal: AbortSignal.timeout(1e4)
|
|
2772
|
+
});
|
|
2773
|
+
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
2774
|
+
}
|
|
2775
|
+
async function checkForUpdate(currentVersion, logger) {
|
|
2776
|
+
try {
|
|
2777
|
+
const res = await fetch("https://registry.npmjs.org/@cfio/cohort-sync/latest", {
|
|
2778
|
+
signal: AbortSignal.timeout(5e3)
|
|
2779
|
+
});
|
|
2780
|
+
if (!res.ok) return;
|
|
2781
|
+
const data = await res.json();
|
|
2782
|
+
const latest = data.version;
|
|
2783
|
+
if (latest && latest !== currentVersion) {
|
|
2784
|
+
logger.warn(
|
|
2785
|
+
`cohort-sync: update available (${currentVersion} \u2192 ${latest}) \u2014 run "npm install -g @cfio/cohort-sync" to update`
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
} catch {
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
async function syncAgentStatus(agentName, status, model, cfg, logger) {
|
|
2792
|
+
try {
|
|
2793
|
+
const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
2794
|
+
const agents = data?.data ?? [];
|
|
2795
|
+
const agent = agents.find(
|
|
2796
|
+
(a) => a.name.toLowerCase() === agentName.toLowerCase()
|
|
2797
|
+
);
|
|
2798
|
+
if (!agent) {
|
|
2799
|
+
const available = agents.map((a) => a.name).join(", ") || "(none)";
|
|
2800
|
+
logger.warn(
|
|
2801
|
+
`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.`
|
|
2802
|
+
);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
2806
|
+
status: normalizeStatus(status),
|
|
2807
|
+
model
|
|
2808
|
+
});
|
|
2809
|
+
logger.info(`cohort-sync: agent "${agentName}" \u2192 ${normalizeStatus(status)}`);
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
logger.warn(`cohort-sync: syncAgentStatus failed: ${String(err)}`);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
async function syncSkillsToV1(skills, cfg, logger) {
|
|
2815
|
+
for (const skill of skills) {
|
|
2816
|
+
try {
|
|
2817
|
+
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills", {
|
|
2818
|
+
name: skill.name,
|
|
2819
|
+
description: skill.description
|
|
2820
|
+
});
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
logger.warn(`cohort-sync: failed to sync skill "${skill.name}": ${String(err)}`);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
var lastKnownRoster = [];
|
|
2827
|
+
function getLastKnownRoster() {
|
|
2828
|
+
return lastKnownRoster;
|
|
2829
|
+
}
|
|
2830
|
+
async function reconcileRoster(openClawAgents, cfg, logger) {
|
|
2831
|
+
const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
2832
|
+
const cohortAgents = data?.data ?? [];
|
|
2833
|
+
const cohortByName = new Map(cohortAgents.map((a) => [a.name.toLowerCase(), a]));
|
|
2834
|
+
const openClawNames = new Set(
|
|
2835
|
+
openClawAgents.map((a) => {
|
|
2836
|
+
const nameMap = cfg.agentNameMap;
|
|
2837
|
+
return (nameMap?.[a.id] ?? a.identity?.name ?? a.id).toLowerCase();
|
|
2838
|
+
})
|
|
2839
|
+
);
|
|
2840
|
+
for (const oc of openClawAgents) {
|
|
2841
|
+
const agentName = (cfg.agentNameMap?.[oc.id] ?? oc.identity?.name ?? oc.id).toLowerCase();
|
|
2842
|
+
const existing = cohortByName.get(agentName);
|
|
2843
|
+
if (!existing) {
|
|
2844
|
+
try {
|
|
2845
|
+
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/agents", {
|
|
2846
|
+
name: agentName,
|
|
2847
|
+
displayName: oc.identity?.name ?? agentName,
|
|
2848
|
+
emoji: oc.identity?.emoji ?? "\u{1F916}",
|
|
2849
|
+
model: oc.model,
|
|
2850
|
+
status: "idle"
|
|
2851
|
+
});
|
|
2852
|
+
logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
logger.warn(`cohort-sync: failed to provision agent "${agentName}": ${String(err)}`);
|
|
2855
|
+
}
|
|
2856
|
+
} else {
|
|
2857
|
+
const updates = {
|
|
2858
|
+
model: oc.model,
|
|
2859
|
+
status: "idle"
|
|
2860
|
+
};
|
|
2861
|
+
if (oc.identity?.name) {
|
|
2862
|
+
updates.displayName = oc.identity.name;
|
|
2863
|
+
}
|
|
2864
|
+
if (oc.identity?.emoji) {
|
|
2865
|
+
updates.emoji = oc.identity.emoji;
|
|
2866
|
+
}
|
|
2867
|
+
try {
|
|
2868
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${existing.id}`, updates);
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
logger.warn(`cohort-sync: failed to update agent "${agentName}": ${String(err)}`);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
for (const cohort of cohortAgents) {
|
|
2875
|
+
if (!openClawNames.has(cohort.name.toLowerCase())) {
|
|
2876
|
+
if (cohort.status === "archived" || cohort.status === "deleted" || cohort.status === "unreachable") {
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
try {
|
|
2880
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${cohort.id}`, {
|
|
2881
|
+
status: "unreachable"
|
|
2882
|
+
});
|
|
2883
|
+
logger.info(`cohort-sync: marked agent "${cohort.name}" as unreachable`);
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
logger.warn(
|
|
2886
|
+
`cohort-sync: failed to mark agent "${cohort.name}" as unreachable: ${String(err)}`
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
const updatedData = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
2892
|
+
const finalRoster = updatedData?.data ?? cohortAgents;
|
|
2893
|
+
lastKnownRoster = finalRoster;
|
|
2894
|
+
return finalRoster;
|
|
2895
|
+
}
|
|
2896
|
+
async function markAllUnreachable(cfg, logger) {
|
|
2897
|
+
const roster = getLastKnownRoster();
|
|
2898
|
+
if (roster.length === 0) {
|
|
2899
|
+
logger.warn("cohort-sync: no cached roster \u2014 skipping markAllUnreachable");
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
for (const agent of roster) {
|
|
2903
|
+
if (agent.status === "unreachable" || agent.status === "archived" || agent.status === "deleted") {
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
try {
|
|
2907
|
+
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
2908
|
+
status: "unreachable"
|
|
2909
|
+
});
|
|
2910
|
+
} catch (err) {
|
|
2911
|
+
logger.warn(`cohort-sync: failed to mark "${agent.name}" as unreachable: ${String(err)}`);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
logger.info("cohort-sync: all agents marked unreachable");
|
|
2915
|
+
}
|
|
2916
|
+
async function fullSync(agentName, model, cfg, logger, openClawAgents) {
|
|
2917
|
+
logger.info("cohort-sync: full sync starting");
|
|
2918
|
+
if (openClawAgents && openClawAgents.length > 0) {
|
|
2919
|
+
try {
|
|
2920
|
+
await reconcileRoster(openClawAgents, cfg, logger);
|
|
2921
|
+
} catch (err) {
|
|
2922
|
+
logger.warn(`cohort-sync: roster reconciliation failed: ${String(err)}`);
|
|
2923
|
+
}
|
|
2924
|
+
} else {
|
|
2925
|
+
await syncAgentStatus(agentName, "working", model, cfg, logger);
|
|
2926
|
+
}
|
|
2927
|
+
const skills = fetchSkills(logger);
|
|
2928
|
+
if (skills.length > 0) {
|
|
2929
|
+
await syncSkillsToV1(skills, cfg, logger);
|
|
2930
|
+
}
|
|
2931
|
+
logger.info("cohort-sync: full sync complete");
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// src/convex-bridge.ts
|
|
2935
|
+
import { createHash } from "crypto";
|
|
2713
2936
|
|
|
2714
2937
|
// ../../node_modules/.pnpm/convex@1.33.0_patch_hash=l43bztwr6e2lbmpd6ao6hmcg24_react@19.2.1/node_modules/convex/dist/esm/index.js
|
|
2715
2938
|
var version = "1.33.0";
|
|
@@ -7641,14 +7864,14 @@ var require_node_gyp_build = __commonJS({
|
|
|
7641
7864
|
"../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
7642
7865
|
var fs4 = __require("fs");
|
|
7643
7866
|
var path4 = __require("path");
|
|
7644
|
-
var
|
|
7867
|
+
var os5 = __require("os");
|
|
7645
7868
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
7646
7869
|
var vars = process.config && process.config.variables || {};
|
|
7647
7870
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
7648
7871
|
var abi = process.versions.modules;
|
|
7649
7872
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
7650
|
-
var arch = process.env.npm_config_arch ||
|
|
7651
|
-
var platform = process.env.npm_config_platform ||
|
|
7873
|
+
var arch = process.env.npm_config_arch || os5.arch();
|
|
7874
|
+
var platform = process.env.npm_config_platform || os5.platform();
|
|
7652
7875
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
7653
7876
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
7654
7877
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -10000,7 +10223,7 @@ var require_websocket = __commonJS({
|
|
|
10000
10223
|
var http = __require("http");
|
|
10001
10224
|
var net = __require("net");
|
|
10002
10225
|
var tls = __require("tls");
|
|
10003
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
10226
|
+
var { randomBytes, createHash: createHash2 } = __require("crypto");
|
|
10004
10227
|
var { Duplex, Readable } = __require("stream");
|
|
10005
10228
|
var { URL: URL2 } = __require("url");
|
|
10006
10229
|
var PerMessageDeflate = require_permessage_deflate();
|
|
@@ -10657,7 +10880,7 @@ var require_websocket = __commonJS({
|
|
|
10657
10880
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
10658
10881
|
return;
|
|
10659
10882
|
}
|
|
10660
|
-
const digest =
|
|
10883
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
10661
10884
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
10662
10885
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
10663
10886
|
return;
|
|
@@ -10922,7 +11145,7 @@ var require_websocket_server = __commonJS({
|
|
|
10922
11145
|
var EventEmitter = __require("events");
|
|
10923
11146
|
var http = __require("http");
|
|
10924
11147
|
var { Duplex } = __require("stream");
|
|
10925
|
-
var { createHash } = __require("crypto");
|
|
11148
|
+
var { createHash: createHash2 } = __require("crypto");
|
|
10926
11149
|
var extension = require_extension();
|
|
10927
11150
|
var PerMessageDeflate = require_permessage_deflate();
|
|
10928
11151
|
var subprotocol = require_subprotocol();
|
|
@@ -11217,7 +11440,7 @@ var require_websocket_server = __commonJS({
|
|
|
11217
11440
|
);
|
|
11218
11441
|
}
|
|
11219
11442
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
11220
|
-
const digest =
|
|
11443
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
11221
11444
|
const headers = [
|
|
11222
11445
|
"HTTP/1.1 101 Switching Protocols",
|
|
11223
11446
|
"Upgrade: websocket",
|
|
@@ -11553,130 +11776,98 @@ function reverseResolveAgentName(cohortName, forwardMap) {
|
|
|
11553
11776
|
return cohortName;
|
|
11554
11777
|
}
|
|
11555
11778
|
|
|
11556
|
-
// src/
|
|
11779
|
+
// src/commands.ts
|
|
11780
|
+
async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger) {
|
|
11781
|
+
if (cmd.type === "restart") {
|
|
11782
|
+
logger.info("cohort-sync: restart command, terminating in 500ms");
|
|
11783
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
11784
|
+
process.kill(process.pid, "SIGTERM");
|
|
11785
|
+
return;
|
|
11786
|
+
}
|
|
11787
|
+
if (cmd.type.startsWith("cron")) {
|
|
11788
|
+
if (!gwClient || !gwClient.isAlive()) {
|
|
11789
|
+
logger.warn(`cohort-sync: no gateway client, cannot execute ${cmd.type}`);
|
|
11790
|
+
return;
|
|
11791
|
+
}
|
|
11792
|
+
const nameMap = cfg.agentNameMap ?? {};
|
|
11793
|
+
switch (cmd.type) {
|
|
11794
|
+
case "cronEnable":
|
|
11795
|
+
await gwClient.request("cron.update", {
|
|
11796
|
+
jobId: cmd.payload?.jobId,
|
|
11797
|
+
patch: { enabled: true }
|
|
11798
|
+
});
|
|
11799
|
+
break;
|
|
11800
|
+
case "cronDisable":
|
|
11801
|
+
await gwClient.request("cron.update", {
|
|
11802
|
+
jobId: cmd.payload?.jobId,
|
|
11803
|
+
patch: { enabled: false }
|
|
11804
|
+
});
|
|
11805
|
+
break;
|
|
11806
|
+
case "cronDelete":
|
|
11807
|
+
await gwClient.request("cron.remove", {
|
|
11808
|
+
jobId: cmd.payload?.jobId
|
|
11809
|
+
});
|
|
11810
|
+
break;
|
|
11811
|
+
case "cronRunNow": {
|
|
11812
|
+
await gwClient.request("cron.run", { jobId: cmd.payload?.jobId });
|
|
11813
|
+
break;
|
|
11814
|
+
}
|
|
11815
|
+
case "cronCreate": {
|
|
11816
|
+
const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
|
|
11817
|
+
await gwClient.request("cron.add", {
|
|
11818
|
+
job: {
|
|
11819
|
+
agentId,
|
|
11820
|
+
name: cmd.payload?.name,
|
|
11821
|
+
enabled: true,
|
|
11822
|
+
schedule: cmd.payload?.schedule,
|
|
11823
|
+
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11824
|
+
sessionTarget: "isolated",
|
|
11825
|
+
wakeMode: "now"
|
|
11826
|
+
}
|
|
11827
|
+
});
|
|
11828
|
+
break;
|
|
11829
|
+
}
|
|
11830
|
+
case "cronUpdate": {
|
|
11831
|
+
const patch = {};
|
|
11832
|
+
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11833
|
+
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11834
|
+
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11835
|
+
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11836
|
+
await gwClient.request("cron.update", {
|
|
11837
|
+
jobId: cmd.payload?.jobId,
|
|
11838
|
+
patch
|
|
11839
|
+
});
|
|
11840
|
+
break;
|
|
11841
|
+
}
|
|
11842
|
+
default:
|
|
11843
|
+
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11844
|
+
}
|
|
11845
|
+
if (gwClient.isAlive()) {
|
|
11846
|
+
try {
|
|
11847
|
+
const snapResult = await gwClient.request("cron.list", { includeDisabled: true });
|
|
11848
|
+
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11849
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11850
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11851
|
+
} catch (snapErr) {
|
|
11852
|
+
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11853
|
+
}
|
|
11854
|
+
}
|
|
11855
|
+
return;
|
|
11856
|
+
}
|
|
11857
|
+
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
11858
|
+
}
|
|
11859
|
+
|
|
11860
|
+
// src/convex-bridge.ts
|
|
11861
|
+
function hashApiKey(key) {
|
|
11862
|
+
return createHash("sha256").update(key).digest("hex");
|
|
11863
|
+
}
|
|
11557
11864
|
function deriveConvexUrl(apiUrl) {
|
|
11865
|
+
const normalized = apiUrl.replace(/\/+$/, "");
|
|
11866
|
+
if (/^https?:\/\/api\.cohort\.bot$/i.test(normalized)) {
|
|
11867
|
+
return normalized.replace(/api\.cohort\.bot$/i, "ws.cohort.bot");
|
|
11868
|
+
}
|
|
11558
11869
|
return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
|
|
11559
11870
|
}
|
|
11560
|
-
var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
|
|
11561
|
-
- Does your planned response address the task's stated scope? If not, do not comment.
|
|
11562
|
-
- Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
|
|
11563
|
-
- If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
|
|
11564
|
-
function buildNotificationMessage(n) {
|
|
11565
|
-
let header;
|
|
11566
|
-
let cta;
|
|
11567
|
-
switch (n.type) {
|
|
11568
|
-
case "comment":
|
|
11569
|
-
if (n.isMentioned) {
|
|
11570
|
-
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
11571
|
-
By: ${n.actorName}`;
|
|
11572
|
-
cta = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
|
|
11573
|
-
} else {
|
|
11574
|
-
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
11575
|
-
From: ${n.actorName}`;
|
|
11576
|
-
cta = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
|
|
11577
|
-
}
|
|
11578
|
-
break;
|
|
11579
|
-
case "assignment":
|
|
11580
|
-
header = `You were assigned to task #${n.taskNumber} "${n.taskTitle}"
|
|
11581
|
-
By: ${n.actorName}`;
|
|
11582
|
-
cta = "Review the task description and begin working on it.";
|
|
11583
|
-
break;
|
|
11584
|
-
case "status_change":
|
|
11585
|
-
header = `Status changed on task #${n.taskNumber} "${n.taskTitle}"
|
|
11586
|
-
By: ${n.actorName}`;
|
|
11587
|
-
cta = "Review the status change and take any follow-up action needed.";
|
|
11588
|
-
break;
|
|
11589
|
-
default:
|
|
11590
|
-
header = `Notification on task #${n.taskNumber} "${n.taskTitle}"
|
|
11591
|
-
From: ${n.actorName}`;
|
|
11592
|
-
cta = "Check the task and respond if needed.";
|
|
11593
|
-
}
|
|
11594
|
-
const body = n.preview ? `
|
|
11595
|
-
Comment: "${n.preview}"` : "";
|
|
11596
|
-
let scope = "";
|
|
11597
|
-
if (n.taskDescription && n.type === "comment") {
|
|
11598
|
-
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
11599
|
-
scope = `
|
|
11600
|
-
|
|
11601
|
-
Scope: ${truncated}`;
|
|
11602
|
-
}
|
|
11603
|
-
const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
|
|
11604
|
-
|
|
11605
|
-
${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
11606
|
-
const promptBlock = n.type === "comment" ? `
|
|
11607
|
-
|
|
11608
|
-
---
|
|
11609
|
-
${prompt}` : "";
|
|
11610
|
-
return `${header}${scope}${body}
|
|
11611
|
-
|
|
11612
|
-
${cta}${promptBlock}`;
|
|
11613
|
-
}
|
|
11614
|
-
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
11615
|
-
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
11616
|
-
method: "POST",
|
|
11617
|
-
headers: {
|
|
11618
|
-
"Content-Type": "application/json",
|
|
11619
|
-
"Authorization": `Bearer ${hooksToken}`
|
|
11620
|
-
},
|
|
11621
|
-
body: JSON.stringify({
|
|
11622
|
-
message: buildNotificationMessage(n),
|
|
11623
|
-
name: "Cohort",
|
|
11624
|
-
agentId,
|
|
11625
|
-
deliver: false,
|
|
11626
|
-
sessionKey: `hook:cohort:task-${n.taskNumber}`
|
|
11627
|
-
})
|
|
11628
|
-
});
|
|
11629
|
-
if (!response.ok) {
|
|
11630
|
-
throw new Error(`/hooks/agent returned ${response.status} ${response.statusText}`);
|
|
11631
|
-
}
|
|
11632
|
-
}
|
|
11633
|
-
var getUndeliveredForPlugin = makeFunctionReference("notifications:getUndeliveredForPlugin");
|
|
11634
|
-
var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredByPlugin");
|
|
11635
|
-
var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
|
|
11636
|
-
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
11637
|
-
var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
|
|
11638
|
-
var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
|
|
11639
|
-
var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
|
|
11640
|
-
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11641
|
-
var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
|
|
11642
|
-
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
11643
|
-
var HOT_KEY = "__cohort_sync__";
|
|
11644
|
-
function getHotState() {
|
|
11645
|
-
let state = globalThis[HOT_KEY];
|
|
11646
|
-
if (!state) {
|
|
11647
|
-
state = {
|
|
11648
|
-
tracker: null,
|
|
11649
|
-
client: null,
|
|
11650
|
-
convexUrl: null,
|
|
11651
|
-
unsubscribers: [],
|
|
11652
|
-
lastKnownRoster: [],
|
|
11653
|
-
intervals: { heartbeat: null, activityFlush: null },
|
|
11654
|
-
activityBuffer: [],
|
|
11655
|
-
channelAgentBridge: {},
|
|
11656
|
-
gatewayPort: null,
|
|
11657
|
-
gatewayToken: null,
|
|
11658
|
-
gatewayProtocolClient: null,
|
|
11659
|
-
commandSubscription: null,
|
|
11660
|
-
cronRunNowPoll: null
|
|
11661
|
-
};
|
|
11662
|
-
globalThis[HOT_KEY] = state;
|
|
11663
|
-
}
|
|
11664
|
-
if (!state.activityBuffer) state.activityBuffer = [];
|
|
11665
|
-
if (!state.intervals) state.intervals = { heartbeat: null, activityFlush: null };
|
|
11666
|
-
if (!state.channelAgentBridge) state.channelAgentBridge = {};
|
|
11667
|
-
if (!state.unsubscribers) state.unsubscribers = [];
|
|
11668
|
-
if (!state.lastKnownRoster) state.lastKnownRoster = [];
|
|
11669
|
-
return state;
|
|
11670
|
-
}
|
|
11671
|
-
function clearHotState() {
|
|
11672
|
-
delete globalThis[HOT_KEY];
|
|
11673
|
-
}
|
|
11674
|
-
function setRosterHotState(roster) {
|
|
11675
|
-
getHotState().lastKnownRoster = roster;
|
|
11676
|
-
}
|
|
11677
|
-
function getRosterHotState() {
|
|
11678
|
-
return getHotState().lastKnownRoster;
|
|
11679
|
-
}
|
|
11680
11871
|
var savedLogger = null;
|
|
11681
11872
|
function setLogger(logger) {
|
|
11682
11873
|
savedLogger = logger;
|
|
@@ -11691,288 +11882,18 @@ function getLogger() {
|
|
|
11691
11882
|
var client = null;
|
|
11692
11883
|
var savedConvexUrl = null;
|
|
11693
11884
|
var unsubscribers = [];
|
|
11694
|
-
function
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
getHotState().convexUrl = url;
|
|
11698
|
-
}
|
|
11699
|
-
function restoreFromHotReload(logger) {
|
|
11700
|
-
const state = getHotState();
|
|
11701
|
-
if (!client && state.client) {
|
|
11702
|
-
client = state.client;
|
|
11703
|
-
savedConvexUrl = state.convexUrl;
|
|
11704
|
-
logger.info("cohort-sync: recovered ConvexClient after hot-reload");
|
|
11705
|
-
} else if (client && state.client && client !== state.client) {
|
|
11706
|
-
try {
|
|
11707
|
-
state.client.close();
|
|
11708
|
-
} catch {
|
|
11709
|
-
}
|
|
11710
|
-
state.client = client;
|
|
11711
|
-
logger.info("cohort-sync: closed orphaned ConvexClient from hot state");
|
|
11885
|
+
function createClient(convexUrl) {
|
|
11886
|
+
if (client) {
|
|
11887
|
+
client.close();
|
|
11712
11888
|
}
|
|
11713
|
-
if (unsubscribers.length === 0 && state.unsubscribers.length > 0) {
|
|
11714
|
-
unsubscribers.push(...state.unsubscribers);
|
|
11715
|
-
logger.info(`cohort-sync: recovered ${state.unsubscribers.length} notification subscriptions after hot-reload`);
|
|
11716
|
-
}
|
|
11717
|
-
}
|
|
11718
|
-
function getOrCreateClient() {
|
|
11719
|
-
if (client) return client;
|
|
11720
|
-
const state = getHotState();
|
|
11721
|
-
if (state.client) {
|
|
11722
|
-
client = state.client;
|
|
11723
|
-
getLogger().info("cohort-sync: recovered ConvexClient from globalThis");
|
|
11724
|
-
return client;
|
|
11725
|
-
}
|
|
11726
|
-
if (!savedConvexUrl) return null;
|
|
11727
|
-
client = new ConvexClient(savedConvexUrl);
|
|
11728
|
-
getLogger().info(`cohort-sync: created fresh ConvexClient (${savedConvexUrl})`);
|
|
11729
|
-
state.client = client;
|
|
11730
|
-
state.convexUrl = savedConvexUrl;
|
|
11731
|
-
return client;
|
|
11732
|
-
}
|
|
11733
|
-
async function initSubscription(port, cfg, hooksToken, logger) {
|
|
11734
|
-
const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
|
|
11735
11889
|
savedConvexUrl = convexUrl;
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
client = state.client;
|
|
11739
|
-
logger.info("cohort-sync: reusing hot-reload ConvexClient for subscription");
|
|
11740
|
-
} else {
|
|
11741
|
-
client = new ConvexClient(convexUrl);
|
|
11742
|
-
logger.info(`cohort-sync: created new ConvexClient for subscription (${convexUrl})`);
|
|
11743
|
-
}
|
|
11744
|
-
state.client = client;
|
|
11745
|
-
state.convexUrl = convexUrl;
|
|
11746
|
-
if (!hooksToken) {
|
|
11747
|
-
logger.warn(
|
|
11748
|
-
`cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
|
|
11749
|
-
);
|
|
11750
|
-
return;
|
|
11751
|
-
}
|
|
11752
|
-
const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
|
|
11753
|
-
const reverseNameMap = {};
|
|
11754
|
-
if (cfg.agentNameMap) {
|
|
11755
|
-
for (const [openclawId, cohortName] of Object.entries(cfg.agentNameMap)) {
|
|
11756
|
-
reverseNameMap[cohortName] = openclawId;
|
|
11757
|
-
}
|
|
11758
|
-
}
|
|
11759
|
-
for (const agentName of agentNames) {
|
|
11760
|
-
const openclawAgentId = reverseNameMap[agentName] ?? agentName;
|
|
11761
|
-
logger.info(`cohort-sync: subscribing to notifications for agent "${agentName}" (openclawId: "${openclawAgentId}")`);
|
|
11762
|
-
let processing = false;
|
|
11763
|
-
const unsubscribe = client.onUpdate(
|
|
11764
|
-
getUndeliveredForPlugin,
|
|
11765
|
-
{ agent: agentName, apiKey: cfg.apiKey },
|
|
11766
|
-
async (notifications) => {
|
|
11767
|
-
if (processing) return;
|
|
11768
|
-
processing = true;
|
|
11769
|
-
try {
|
|
11770
|
-
for (const n of notifications) {
|
|
11771
|
-
try {
|
|
11772
|
-
await injectNotification(port, hooksToken, n, openclawAgentId);
|
|
11773
|
-
logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
|
|
11774
|
-
await client.mutation(markDeliveredByPlugin, {
|
|
11775
|
-
notificationId: n._id,
|
|
11776
|
-
apiKey: cfg.apiKey
|
|
11777
|
-
});
|
|
11778
|
-
} catch (err) {
|
|
11779
|
-
logger.warn(`cohort-sync: failed to inject notification ${n._id}: ${String(err)}`);
|
|
11780
|
-
}
|
|
11781
|
-
}
|
|
11782
|
-
} finally {
|
|
11783
|
-
processing = false;
|
|
11784
|
-
}
|
|
11785
|
-
},
|
|
11786
|
-
(err) => {
|
|
11787
|
-
logger.error(`cohort-sync: subscription error for "${agentName}": ${String(err)}`);
|
|
11788
|
-
}
|
|
11789
|
-
);
|
|
11790
|
-
unsubscribers.push(unsubscribe);
|
|
11791
|
-
}
|
|
11792
|
-
state.unsubscribers = [...unsubscribers];
|
|
11793
|
-
}
|
|
11794
|
-
function initCommandSubscription(cfg, logger, resolveAgentName) {
|
|
11795
|
-
const c = getOrCreateClient();
|
|
11796
|
-
if (!c) {
|
|
11797
|
-
logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
|
|
11798
|
-
return null;
|
|
11799
|
-
}
|
|
11800
|
-
let processing = false;
|
|
11801
|
-
const unsubscribe = c.onUpdate(
|
|
11802
|
-
getPendingCommandsForPlugin,
|
|
11803
|
-
{ apiKey: cfg.apiKey },
|
|
11804
|
-
async (commands) => {
|
|
11805
|
-
if (processing) return;
|
|
11806
|
-
if (commands.length === 0) return;
|
|
11807
|
-
processing = true;
|
|
11808
|
-
try {
|
|
11809
|
-
for (const cmd of commands) {
|
|
11810
|
-
logger.info(`cohort-sync: processing command: ${cmd.type} (${cmd._id})`);
|
|
11811
|
-
try {
|
|
11812
|
-
await c.mutation(acknowledgeCommandRef, {
|
|
11813
|
-
commandId: cmd._id,
|
|
11814
|
-
apiKey: cfg.apiKey
|
|
11815
|
-
});
|
|
11816
|
-
if (cmd.type === "restart") {
|
|
11817
|
-
logger.info("cohort-sync: restart acknowledged, terminating in 500ms");
|
|
11818
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
11819
|
-
process.kill(process.pid, "SIGTERM");
|
|
11820
|
-
return;
|
|
11821
|
-
}
|
|
11822
|
-
if (cmd.type.startsWith("cron")) {
|
|
11823
|
-
const hotState = getHotState();
|
|
11824
|
-
const port = hotState.gatewayPort;
|
|
11825
|
-
const token = hotState.gatewayToken;
|
|
11826
|
-
if (!port || !token) {
|
|
11827
|
-
logger.warn(`cohort-sync: no gateway port/token, cannot execute ${cmd.type}`);
|
|
11828
|
-
continue;
|
|
11829
|
-
}
|
|
11830
|
-
const gwClient = new GatewayClient(port, token, logger);
|
|
11831
|
-
try {
|
|
11832
|
-
await gwClient.connect();
|
|
11833
|
-
const nameMap = cfg.agentNameMap ?? {};
|
|
11834
|
-
switch (cmd.type) {
|
|
11835
|
-
case "cronEnable":
|
|
11836
|
-
await gwClient.request("cron.update", {
|
|
11837
|
-
jobId: cmd.payload?.jobId,
|
|
11838
|
-
patch: { enabled: true }
|
|
11839
|
-
});
|
|
11840
|
-
break;
|
|
11841
|
-
case "cronDisable":
|
|
11842
|
-
await gwClient.request("cron.update", {
|
|
11843
|
-
jobId: cmd.payload?.jobId,
|
|
11844
|
-
patch: { enabled: false }
|
|
11845
|
-
});
|
|
11846
|
-
break;
|
|
11847
|
-
case "cronDelete":
|
|
11848
|
-
await gwClient.request("cron.remove", {
|
|
11849
|
-
jobId: cmd.payload?.jobId
|
|
11850
|
-
});
|
|
11851
|
-
break;
|
|
11852
|
-
case "cronRunNow": {
|
|
11853
|
-
const runResult = await gwClient.request(
|
|
11854
|
-
"cron.run",
|
|
11855
|
-
{ jobId: cmd.payload?.jobId }
|
|
11856
|
-
);
|
|
11857
|
-
if (runResult?.ok && runResult?.ran) {
|
|
11858
|
-
const jobId = cmd.payload?.jobId;
|
|
11859
|
-
let polls = 0;
|
|
11860
|
-
const hs = getHotState();
|
|
11861
|
-
if (hs.cronRunNowPoll) clearInterval(hs.cronRunNowPoll);
|
|
11862
|
-
const pollInterval = setInterval(async () => {
|
|
11863
|
-
polls++;
|
|
11864
|
-
if (polls >= 15) {
|
|
11865
|
-
clearInterval(pollInterval);
|
|
11866
|
-
hs.cronRunNowPoll = null;
|
|
11867
|
-
return;
|
|
11868
|
-
}
|
|
11869
|
-
try {
|
|
11870
|
-
const pollClient = getHotState().gatewayProtocolClient;
|
|
11871
|
-
if (!pollClient || !pollClient.isAlive()) {
|
|
11872
|
-
clearInterval(pollInterval);
|
|
11873
|
-
hs.cronRunNowPoll = null;
|
|
11874
|
-
return;
|
|
11875
|
-
}
|
|
11876
|
-
const pollResult = await pollClient.request("cron.list");
|
|
11877
|
-
const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
|
|
11878
|
-
const job = freshJobs.find((j) => j.id === jobId);
|
|
11879
|
-
if (job && !job.state?.runningAtMs) {
|
|
11880
|
-
clearInterval(pollInterval);
|
|
11881
|
-
hs.cronRunNowPoll = null;
|
|
11882
|
-
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11883
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11884
|
-
}
|
|
11885
|
-
} catch {
|
|
11886
|
-
}
|
|
11887
|
-
}, 2e3);
|
|
11888
|
-
hs.cronRunNowPoll = pollInterval;
|
|
11889
|
-
}
|
|
11890
|
-
break;
|
|
11891
|
-
}
|
|
11892
|
-
case "cronCreate": {
|
|
11893
|
-
const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
|
|
11894
|
-
await gwClient.request("cron.add", {
|
|
11895
|
-
job: {
|
|
11896
|
-
agentId,
|
|
11897
|
-
name: cmd.payload?.name,
|
|
11898
|
-
enabled: true,
|
|
11899
|
-
schedule: cmd.payload?.schedule,
|
|
11900
|
-
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11901
|
-
sessionTarget: "isolated",
|
|
11902
|
-
wakeMode: "now"
|
|
11903
|
-
}
|
|
11904
|
-
});
|
|
11905
|
-
break;
|
|
11906
|
-
}
|
|
11907
|
-
case "cronUpdate": {
|
|
11908
|
-
const patch = {};
|
|
11909
|
-
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11910
|
-
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11911
|
-
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11912
|
-
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11913
|
-
await gwClient.request("cron.update", {
|
|
11914
|
-
jobId: cmd.payload?.jobId,
|
|
11915
|
-
patch
|
|
11916
|
-
});
|
|
11917
|
-
break;
|
|
11918
|
-
}
|
|
11919
|
-
default:
|
|
11920
|
-
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11921
|
-
}
|
|
11922
|
-
if (gwClient.isAlive()) {
|
|
11923
|
-
try {
|
|
11924
|
-
const snapResult = await gwClient.request("cron.list");
|
|
11925
|
-
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11926
|
-
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11927
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11928
|
-
} catch (snapErr) {
|
|
11929
|
-
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11930
|
-
}
|
|
11931
|
-
}
|
|
11932
|
-
} finally {
|
|
11933
|
-
gwClient.close();
|
|
11934
|
-
}
|
|
11935
|
-
} else {
|
|
11936
|
-
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
11937
|
-
}
|
|
11938
|
-
} catch (err) {
|
|
11939
|
-
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
11940
|
-
try {
|
|
11941
|
-
await c.mutation(failCommandRef, {
|
|
11942
|
-
commandId: cmd._id,
|
|
11943
|
-
apiKey: cfg.apiKey,
|
|
11944
|
-
reason: String(err).slice(0, 500)
|
|
11945
|
-
});
|
|
11946
|
-
} catch (failErr) {
|
|
11947
|
-
logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
|
|
11948
|
-
}
|
|
11949
|
-
}
|
|
11950
|
-
}
|
|
11951
|
-
} finally {
|
|
11952
|
-
processing = false;
|
|
11953
|
-
}
|
|
11954
|
-
},
|
|
11955
|
-
(err) => {
|
|
11956
|
-
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
11957
|
-
}
|
|
11958
|
-
);
|
|
11959
|
-
logger.info("cohort-sync: command subscription active");
|
|
11960
|
-
return unsubscribe;
|
|
11890
|
+
client = new ConvexClient(convexUrl);
|
|
11891
|
+
return client;
|
|
11961
11892
|
}
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
if (!c) {
|
|
11965
|
-
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
11966
|
-
}
|
|
11967
|
-
return await c.mutation(addCommentFromPluginRef, {
|
|
11968
|
-
apiKey,
|
|
11969
|
-
taskNumber: args.taskNumber,
|
|
11970
|
-
agentName: args.agentName,
|
|
11971
|
-
content: args.content,
|
|
11972
|
-
noReply: args.noReply
|
|
11973
|
-
});
|
|
11893
|
+
function getClient() {
|
|
11894
|
+
return client;
|
|
11974
11895
|
}
|
|
11975
|
-
function
|
|
11896
|
+
function closeBridge() {
|
|
11976
11897
|
for (const unsub of unsubscribers) {
|
|
11977
11898
|
try {
|
|
11978
11899
|
unsub();
|
|
@@ -11980,327 +11901,258 @@ function closeSubscription() {
|
|
|
11980
11901
|
}
|
|
11981
11902
|
}
|
|
11982
11903
|
unsubscribers.length = 0;
|
|
11983
|
-
|
|
11984
|
-
for (const unsub of state.unsubscribers) {
|
|
11985
|
-
try {
|
|
11986
|
-
unsub();
|
|
11987
|
-
} catch {
|
|
11988
|
-
}
|
|
11989
|
-
}
|
|
11990
|
-
if (state.commandSubscription) {
|
|
11904
|
+
if (commandUnsubscriber) {
|
|
11991
11905
|
try {
|
|
11992
|
-
|
|
11906
|
+
commandUnsubscriber();
|
|
11993
11907
|
} catch {
|
|
11994
11908
|
}
|
|
11995
|
-
|
|
11909
|
+
commandUnsubscriber = null;
|
|
11996
11910
|
}
|
|
11997
|
-
if (
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
} catch {
|
|
12001
|
-
}
|
|
12002
|
-
state.gatewayProtocolClient = null;
|
|
11911
|
+
if (client) {
|
|
11912
|
+
client.close();
|
|
11913
|
+
client = null;
|
|
12003
11914
|
}
|
|
12004
|
-
|
|
12005
|
-
client = null;
|
|
12006
|
-
clearHotState();
|
|
11915
|
+
savedConvexUrl = null;
|
|
12007
11916
|
}
|
|
11917
|
+
var commandUnsubscriber = null;
|
|
11918
|
+
var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
|
|
11919
|
+
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
11920
|
+
var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
|
|
11921
|
+
var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
|
|
11922
|
+
var getUndeliveredForPlugin = makeFunctionReference("notifications:getUndeliveredForPlugin");
|
|
11923
|
+
var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredByPlugin");
|
|
11924
|
+
var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
|
|
11925
|
+
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11926
|
+
var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
|
|
11927
|
+
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
12008
11928
|
async function pushTelemetry(apiKey, data) {
|
|
12009
|
-
const c =
|
|
11929
|
+
const c = getClient();
|
|
12010
11930
|
if (!c) return;
|
|
12011
11931
|
try {
|
|
12012
|
-
await c.mutation(upsertTelemetryFromPlugin, { apiKey, ...data });
|
|
11932
|
+
await c.mutation(upsertTelemetryFromPlugin, { apiKeyHash: hashApiKey(apiKey), ...data });
|
|
12013
11933
|
} catch (err) {
|
|
12014
11934
|
getLogger().error(`cohort-sync: pushTelemetry failed: ${err}`);
|
|
12015
11935
|
}
|
|
12016
11936
|
}
|
|
12017
11937
|
async function pushSessions(apiKey, agentName, sessions) {
|
|
12018
|
-
const c =
|
|
11938
|
+
const c = getClient();
|
|
12019
11939
|
if (!c) return;
|
|
12020
11940
|
try {
|
|
12021
|
-
await c.mutation(upsertSessionsFromPlugin, { apiKey, agentName, sessions });
|
|
11941
|
+
await c.mutation(upsertSessionsFromPlugin, { apiKeyHash: hashApiKey(apiKey), agentName, sessions });
|
|
12022
11942
|
} catch (err) {
|
|
12023
11943
|
getLogger().error(`cohort-sync: pushSessions failed: ${err}`);
|
|
12024
11944
|
}
|
|
12025
11945
|
}
|
|
12026
11946
|
async function pushActivity(apiKey, entries) {
|
|
12027
11947
|
if (entries.length === 0) return;
|
|
12028
|
-
const c =
|
|
11948
|
+
const c = getClient();
|
|
12029
11949
|
if (!c) return;
|
|
12030
11950
|
try {
|
|
12031
|
-
await c.mutation(pushActivityFromPluginRef, { apiKey, entries });
|
|
11951
|
+
await c.mutation(pushActivityFromPluginRef, { apiKeyHash: hashApiKey(apiKey), entries });
|
|
12032
11952
|
} catch (err) {
|
|
12033
11953
|
getLogger().error(`cohort-sync: pushActivity failed: ${err}`);
|
|
12034
11954
|
}
|
|
12035
11955
|
}
|
|
12036
11956
|
async function pushCronSnapshot(apiKey, jobs) {
|
|
12037
|
-
const c =
|
|
12038
|
-
if (!c) return;
|
|
11957
|
+
const c = getClient();
|
|
11958
|
+
if (!c) return false;
|
|
12039
11959
|
try {
|
|
12040
|
-
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKey, jobs });
|
|
11960
|
+
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKeyHash: hashApiKey(apiKey), jobs });
|
|
11961
|
+
return true;
|
|
12041
11962
|
} catch (err) {
|
|
12042
11963
|
getLogger().error(`cohort-sync: pushCronSnapshot failed: ${err}`);
|
|
11964
|
+
return false;
|
|
12043
11965
|
}
|
|
12044
11966
|
}
|
|
12045
|
-
function
|
|
12046
|
-
const
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
function clearIntervalsFromHot() {
|
|
12050
|
-
const state = getHotState();
|
|
12051
|
-
if (state.intervals.heartbeat) clearInterval(state.intervals.heartbeat);
|
|
12052
|
-
if (state.intervals.activityFlush) clearInterval(state.intervals.activityFlush);
|
|
12053
|
-
if (state.cronRunNowPoll) clearInterval(state.cronRunNowPoll);
|
|
12054
|
-
state.intervals = { heartbeat: null, activityFlush: null };
|
|
12055
|
-
state.cronRunNowPoll = null;
|
|
12056
|
-
}
|
|
12057
|
-
function addActivityToHot(entry) {
|
|
12058
|
-
const state = getHotState();
|
|
12059
|
-
state.activityBuffer.push(entry);
|
|
12060
|
-
const logger = savedLogger;
|
|
12061
|
-
if (logger) {
|
|
12062
|
-
logger.info(`cohort-sync: +activity [${entry.category}] "${entry.text}"`);
|
|
12063
|
-
}
|
|
12064
|
-
}
|
|
12065
|
-
function drainActivityFromHot() {
|
|
12066
|
-
const state = getHotState();
|
|
12067
|
-
const buf = state.activityBuffer;
|
|
12068
|
-
state.activityBuffer = [];
|
|
12069
|
-
return buf;
|
|
12070
|
-
}
|
|
12071
|
-
function setChannelAgent(channelId, agentName) {
|
|
12072
|
-
getHotState().channelAgentBridge[channelId] = agentName;
|
|
12073
|
-
}
|
|
12074
|
-
function getChannelAgent(channelId) {
|
|
12075
|
-
return getHotState().channelAgentBridge[channelId] ?? null;
|
|
12076
|
-
}
|
|
12077
|
-
|
|
12078
|
-
// src/sync.ts
|
|
12079
|
-
function extractJson(raw) {
|
|
12080
|
-
const jsonStart = raw.search(/[\[{]/);
|
|
12081
|
-
const jsonEndBracket = raw.lastIndexOf("]");
|
|
12082
|
-
const jsonEndBrace = raw.lastIndexOf("}");
|
|
12083
|
-
const jsonEnd = Math.max(jsonEndBracket, jsonEndBrace);
|
|
12084
|
-
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
|
|
12085
|
-
throw new Error("No JSON found in output");
|
|
12086
|
-
}
|
|
12087
|
-
return raw.slice(jsonStart, jsonEnd + 1);
|
|
12088
|
-
}
|
|
12089
|
-
function fetchSkills(logger) {
|
|
12090
|
-
try {
|
|
12091
|
-
const raw = execSync("openclaw skills list --json", {
|
|
12092
|
-
encoding: "utf8",
|
|
12093
|
-
timeout: 3e4,
|
|
12094
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
12095
|
-
env: { ...process.env, NO_COLOR: "1" }
|
|
12096
|
-
});
|
|
12097
|
-
const parsed = JSON.parse(extractJson(raw));
|
|
12098
|
-
const list = Array.isArray(parsed) ? parsed : parsed?.skills ?? [];
|
|
12099
|
-
return list.map((s) => ({
|
|
12100
|
-
name: String(s.name ?? s.id ?? "unknown"),
|
|
12101
|
-
description: String(s.description ?? ""),
|
|
12102
|
-
source: String(s.source ?? s.origin ?? "unknown"),
|
|
12103
|
-
...s.emoji ? { emoji: String(s.emoji) } : {}
|
|
12104
|
-
}));
|
|
12105
|
-
} catch (err) {
|
|
12106
|
-
logger.warn(`cohort-sync: failed to fetch skills: ${String(err)}`);
|
|
12107
|
-
return [];
|
|
11967
|
+
async function callAddCommentFromPlugin(apiKey, args) {
|
|
11968
|
+
const c = getClient();
|
|
11969
|
+
if (!c) {
|
|
11970
|
+
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
12108
11971
|
}
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12116
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
12117
|
-
signal: AbortSignal.timeout(1e4)
|
|
12118
|
-
});
|
|
12119
|
-
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
12120
|
-
return res.json();
|
|
12121
|
-
}
|
|
12122
|
-
async function v1Patch(apiUrl, apiKey, path4, body) {
|
|
12123
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12124
|
-
method: "PATCH",
|
|
12125
|
-
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12126
|
-
body: JSON.stringify(body),
|
|
12127
|
-
signal: AbortSignal.timeout(1e4)
|
|
12128
|
-
});
|
|
12129
|
-
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
12130
|
-
}
|
|
12131
|
-
async function v1Post(apiUrl, apiKey, path4, body) {
|
|
12132
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12133
|
-
method: "POST",
|
|
12134
|
-
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12135
|
-
body: JSON.stringify(body),
|
|
12136
|
-
signal: AbortSignal.timeout(1e4)
|
|
11972
|
+
return await c.mutation(addCommentFromPluginRef, {
|
|
11973
|
+
apiKeyHash: hashApiKey(apiKey),
|
|
11974
|
+
taskNumber: args.taskNumber,
|
|
11975
|
+
agentName: args.agentName,
|
|
11976
|
+
content: args.content,
|
|
11977
|
+
noReply: args.noReply
|
|
12137
11978
|
});
|
|
12138
|
-
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
12139
|
-
}
|
|
12140
|
-
async function checkForUpdate(currentVersion, logger) {
|
|
12141
|
-
try {
|
|
12142
|
-
const res = await fetch("https://registry.npmjs.org/@cfio/cohort-sync/latest", {
|
|
12143
|
-
signal: AbortSignal.timeout(5e3)
|
|
12144
|
-
});
|
|
12145
|
-
if (!res.ok) return;
|
|
12146
|
-
const data = await res.json();
|
|
12147
|
-
const latest = data.version;
|
|
12148
|
-
if (latest && latest !== currentVersion) {
|
|
12149
|
-
logger.warn(
|
|
12150
|
-
`cohort-sync: update available (${currentVersion} \u2192 ${latest}) \u2014 run "npm install -g @cfio/cohort-sync" to update`
|
|
12151
|
-
);
|
|
12152
|
-
}
|
|
12153
|
-
} catch {
|
|
12154
|
-
}
|
|
12155
|
-
}
|
|
12156
|
-
async function syncAgentStatus(agentName, status, model, cfg, logger) {
|
|
12157
|
-
try {
|
|
12158
|
-
const data = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
12159
|
-
const agents = data?.data ?? [];
|
|
12160
|
-
const agent = agents.find(
|
|
12161
|
-
(a) => a.name.toLowerCase() === agentName.toLowerCase()
|
|
12162
|
-
);
|
|
12163
|
-
if (!agent) {
|
|
12164
|
-
const available = agents.map((a) => a.name).join(", ") || "(none)";
|
|
12165
|
-
logger.warn(
|
|
12166
|
-
`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.`
|
|
12167
|
-
);
|
|
12168
|
-
return;
|
|
12169
|
-
}
|
|
12170
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
12171
|
-
status: normalizeStatus(status),
|
|
12172
|
-
model
|
|
12173
|
-
});
|
|
12174
|
-
logger.info(`cohort-sync: agent "${agentName}" \u2192 ${normalizeStatus(status)}`);
|
|
12175
|
-
} catch (err) {
|
|
12176
|
-
logger.warn(`cohort-sync: syncAgentStatus failed: ${String(err)}`);
|
|
12177
|
-
}
|
|
12178
11979
|
}
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
11980
|
+
var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
|
|
11981
|
+
- Does your planned response address the task's stated scope? If not, do not comment.
|
|
11982
|
+
- Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
|
|
11983
|
+
- If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
|
|
11984
|
+
function buildNotificationMessage(n) {
|
|
11985
|
+
let header;
|
|
11986
|
+
let cta;
|
|
11987
|
+
switch (n.type) {
|
|
11988
|
+
case "comment":
|
|
11989
|
+
if (n.isMentioned) {
|
|
11990
|
+
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
11991
|
+
By: ${n.actorName}`;
|
|
11992
|
+
cta = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
|
|
11993
|
+
} else {
|
|
11994
|
+
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
11995
|
+
From: ${n.actorName}`;
|
|
11996
|
+
cta = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
|
|
11997
|
+
}
|
|
11998
|
+
break;
|
|
11999
|
+
case "assignment":
|
|
12000
|
+
header = `You were assigned to task #${n.taskNumber} "${n.taskTitle}"
|
|
12001
|
+
By: ${n.actorName}`;
|
|
12002
|
+
cta = "Review the task description and begin working on it.";
|
|
12003
|
+
break;
|
|
12004
|
+
case "status_change":
|
|
12005
|
+
header = `Status changed on task #${n.taskNumber} "${n.taskTitle}"
|
|
12006
|
+
By: ${n.actorName}`;
|
|
12007
|
+
cta = "Review the status change and take any follow-up action needed.";
|
|
12008
|
+
break;
|
|
12009
|
+
default:
|
|
12010
|
+
header = `Notification on task #${n.taskNumber} "${n.taskTitle}"
|
|
12011
|
+
From: ${n.actorName}`;
|
|
12012
|
+
cta = "Check the task and respond if needed.";
|
|
12189
12013
|
}
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
logger.info(`cohort-sync: recovered roster (${hotRoster.length} agents) after hot-reload`);
|
|
12014
|
+
const body = n.preview ? `
|
|
12015
|
+
Comment: "${n.preview}"` : "";
|
|
12016
|
+
let scope = "";
|
|
12017
|
+
if (n.taskDescription && n.type === "comment") {
|
|
12018
|
+
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
12019
|
+
scope = `
|
|
12020
|
+
|
|
12021
|
+
Scope: ${truncated}`;
|
|
12199
12022
|
}
|
|
12023
|
+
const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
|
|
12024
|
+
|
|
12025
|
+
${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
12026
|
+
const promptBlock = n.type === "comment" ? `
|
|
12027
|
+
|
|
12028
|
+
---
|
|
12029
|
+
${prompt}` : "";
|
|
12030
|
+
return `${header}${scope}${body}
|
|
12031
|
+
|
|
12032
|
+
${cta}${promptBlock}`;
|
|
12200
12033
|
}
|
|
12201
|
-
async function
|
|
12202
|
-
const
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
displayName: oc.identity?.name ?? agentName,
|
|
12219
|
-
emoji: oc.identity?.emoji ?? "\u{1F916}",
|
|
12220
|
-
model: oc.model,
|
|
12221
|
-
status: "idle"
|
|
12222
|
-
});
|
|
12223
|
-
logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
|
|
12224
|
-
} catch (err) {
|
|
12225
|
-
logger.warn(`cohort-sync: failed to provision agent "${agentName}": ${String(err)}`);
|
|
12226
|
-
}
|
|
12227
|
-
} else {
|
|
12228
|
-
const updates = {
|
|
12229
|
-
model: oc.model,
|
|
12230
|
-
status: "idle"
|
|
12231
|
-
};
|
|
12232
|
-
if (oc.identity?.name) {
|
|
12233
|
-
updates.displayName = oc.identity.name;
|
|
12234
|
-
}
|
|
12235
|
-
if (oc.identity?.emoji) {
|
|
12236
|
-
updates.emoji = oc.identity.emoji;
|
|
12237
|
-
}
|
|
12238
|
-
try {
|
|
12239
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${existing.id}`, updates);
|
|
12240
|
-
} catch (err) {
|
|
12241
|
-
logger.warn(`cohort-sync: failed to update agent "${agentName}": ${String(err)}`);
|
|
12242
|
-
}
|
|
12243
|
-
}
|
|
12244
|
-
}
|
|
12245
|
-
for (const cohort of cohortAgents) {
|
|
12246
|
-
if (!openClawNames.has(cohort.name.toLowerCase())) {
|
|
12247
|
-
if (cohort.status === "archived" || cohort.status === "deleted" || cohort.status === "unreachable") {
|
|
12248
|
-
continue;
|
|
12249
|
-
}
|
|
12250
|
-
try {
|
|
12251
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${cohort.id}`, {
|
|
12252
|
-
status: "unreachable"
|
|
12253
|
-
});
|
|
12254
|
-
logger.info(`cohort-sync: marked agent "${cohort.name}" as unreachable`);
|
|
12255
|
-
} catch (err) {
|
|
12256
|
-
logger.warn(
|
|
12257
|
-
`cohort-sync: failed to mark agent "${cohort.name}" as unreachable: ${String(err)}`
|
|
12258
|
-
);
|
|
12259
|
-
}
|
|
12260
|
-
}
|
|
12034
|
+
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
12035
|
+
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
12036
|
+
method: "POST",
|
|
12037
|
+
headers: {
|
|
12038
|
+
"Content-Type": "application/json",
|
|
12039
|
+
"Authorization": `Bearer ${hooksToken}`
|
|
12040
|
+
},
|
|
12041
|
+
body: JSON.stringify({
|
|
12042
|
+
message: buildNotificationMessage(n),
|
|
12043
|
+
name: "Cohort",
|
|
12044
|
+
agentId,
|
|
12045
|
+
deliver: false,
|
|
12046
|
+
sessionKey: `hook:cohort:task-${n.taskNumber}`
|
|
12047
|
+
})
|
|
12048
|
+
});
|
|
12049
|
+
if (!response.ok) {
|
|
12050
|
+
throw new Error(`/hooks/agent returned ${response.status} ${response.statusText}`);
|
|
12261
12051
|
}
|
|
12262
|
-
const updatedData = await v1Get(cfg.apiUrl, cfg.apiKey, "/api/v1/agents?limit=100");
|
|
12263
|
-
const finalRoster = updatedData?.data ?? cohortAgents;
|
|
12264
|
-
lastKnownRoster = finalRoster;
|
|
12265
|
-
setRosterHotState(finalRoster);
|
|
12266
|
-
return finalRoster;
|
|
12267
12052
|
}
|
|
12268
|
-
async function
|
|
12269
|
-
const
|
|
12270
|
-
if (
|
|
12271
|
-
logger.warn("cohort-sync: no
|
|
12053
|
+
async function startNotificationSubscription(port, cfg, hooksToken, logger) {
|
|
12054
|
+
const c = getClient();
|
|
12055
|
+
if (!c) {
|
|
12056
|
+
logger.warn("cohort-sync: no ConvexClient \u2014 notification subscription skipped");
|
|
12272
12057
|
return;
|
|
12273
12058
|
}
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
await v1Patch(cfg.apiUrl, cfg.apiKey, `/api/v1/agents/${agent.id}`, {
|
|
12280
|
-
status: "unreachable"
|
|
12281
|
-
});
|
|
12282
|
-
} catch (err) {
|
|
12283
|
-
logger.warn(`cohort-sync: failed to mark "${agent.name}" as unreachable: ${String(err)}`);
|
|
12284
|
-
}
|
|
12059
|
+
if (!hooksToken) {
|
|
12060
|
+
logger.warn(
|
|
12061
|
+
`cohort-sync: hooks.token not configured \u2014 real-time notifications disabled (telemetry will still be pushed).`
|
|
12062
|
+
);
|
|
12063
|
+
return;
|
|
12285
12064
|
}
|
|
12286
|
-
|
|
12287
|
-
}
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
try {
|
|
12292
|
-
await reconcileRoster(openClawAgents, cfg, logger);
|
|
12293
|
-
} catch (err) {
|
|
12294
|
-
logger.warn(`cohort-sync: roster reconciliation failed: ${String(err)}`);
|
|
12065
|
+
const agentNames = cfg.agentNameMap ? Object.values(cfg.agentNameMap) : ["main"];
|
|
12066
|
+
const reverseNameMap = {};
|
|
12067
|
+
if (cfg.agentNameMap) {
|
|
12068
|
+
for (const [openclawId, cohortName] of Object.entries(cfg.agentNameMap)) {
|
|
12069
|
+
reverseNameMap[cohortName] = openclawId;
|
|
12295
12070
|
}
|
|
12296
|
-
} else {
|
|
12297
|
-
await syncAgentStatus(agentName, "working", model, cfg, logger);
|
|
12298
12071
|
}
|
|
12299
|
-
const
|
|
12300
|
-
|
|
12301
|
-
|
|
12072
|
+
for (const agentName of agentNames) {
|
|
12073
|
+
const openclawAgentId = reverseNameMap[agentName] ?? agentName;
|
|
12074
|
+
logger.info(`cohort-sync: subscribing to notifications for agent "${agentName}" (openclawId: "${openclawAgentId}")`);
|
|
12075
|
+
let processing = false;
|
|
12076
|
+
const apiKeyHash = hashApiKey(cfg.apiKey);
|
|
12077
|
+
const unsubscribe = c.onUpdate(
|
|
12078
|
+
getUndeliveredForPlugin,
|
|
12079
|
+
{ agent: agentName, apiKeyHash },
|
|
12080
|
+
async (notifications) => {
|
|
12081
|
+
if (processing) return;
|
|
12082
|
+
processing = true;
|
|
12083
|
+
try {
|
|
12084
|
+
for (const n of notifications) {
|
|
12085
|
+
try {
|
|
12086
|
+
await injectNotification(port, hooksToken, n, openclawAgentId);
|
|
12087
|
+
logger.info(`cohort-sync: injected notification for task #${n.taskNumber} (${agentName})`);
|
|
12088
|
+
await c.mutation(markDeliveredByPlugin, {
|
|
12089
|
+
notificationId: n._id,
|
|
12090
|
+
apiKeyHash
|
|
12091
|
+
});
|
|
12092
|
+
} catch (err) {
|
|
12093
|
+
logger.warn(`cohort-sync: failed to inject notification ${n._id}: ${String(err)}`);
|
|
12094
|
+
}
|
|
12095
|
+
}
|
|
12096
|
+
} finally {
|
|
12097
|
+
processing = false;
|
|
12098
|
+
}
|
|
12099
|
+
},
|
|
12100
|
+
(err) => {
|
|
12101
|
+
logger.error(`cohort-sync: subscription error for "${agentName}": ${String(err)}`);
|
|
12102
|
+
}
|
|
12103
|
+
);
|
|
12104
|
+
unsubscribers.push(unsubscribe);
|
|
12302
12105
|
}
|
|
12303
|
-
|
|
12106
|
+
}
|
|
12107
|
+
function startCommandSubscription(cfg, logger, resolveAgentName, gwClient) {
|
|
12108
|
+
const c = getClient();
|
|
12109
|
+
if (!c) {
|
|
12110
|
+
logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
|
|
12111
|
+
return null;
|
|
12112
|
+
}
|
|
12113
|
+
let processing = false;
|
|
12114
|
+
const apiKeyHash = hashApiKey(cfg.apiKey);
|
|
12115
|
+
const unsubscribe = c.onUpdate(
|
|
12116
|
+
getPendingCommandsForPlugin,
|
|
12117
|
+
{ apiKeyHash },
|
|
12118
|
+
async (commands) => {
|
|
12119
|
+
if (processing) return;
|
|
12120
|
+
if (commands.length === 0) return;
|
|
12121
|
+
processing = true;
|
|
12122
|
+
try {
|
|
12123
|
+
for (const cmd of commands) {
|
|
12124
|
+
logger.info(`cohort-sync: processing command: ${cmd.type} (${cmd._id})`);
|
|
12125
|
+
try {
|
|
12126
|
+
await c.mutation(acknowledgeCommandRef, {
|
|
12127
|
+
commandId: cmd._id,
|
|
12128
|
+
apiKeyHash
|
|
12129
|
+
});
|
|
12130
|
+
await executeCommand(cmd, gwClient, cfg, resolveAgentName, logger);
|
|
12131
|
+
if (cmd.type === "restart") return;
|
|
12132
|
+
} catch (err) {
|
|
12133
|
+
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
12134
|
+
try {
|
|
12135
|
+
await c.mutation(failCommandRef, {
|
|
12136
|
+
commandId: cmd._id,
|
|
12137
|
+
apiKeyHash,
|
|
12138
|
+
reason: String(err).slice(0, 500)
|
|
12139
|
+
});
|
|
12140
|
+
} catch (failErr) {
|
|
12141
|
+
logger.error(`cohort-sync: failed to mark command ${cmd._id} as failed: ${String(failErr)}`);
|
|
12142
|
+
}
|
|
12143
|
+
}
|
|
12144
|
+
}
|
|
12145
|
+
} finally {
|
|
12146
|
+
processing = false;
|
|
12147
|
+
}
|
|
12148
|
+
},
|
|
12149
|
+
(err) => {
|
|
12150
|
+
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
12151
|
+
}
|
|
12152
|
+
);
|
|
12153
|
+
commandUnsubscriber = unsubscribe;
|
|
12154
|
+
logger.info("cohort-sync: command subscription active");
|
|
12155
|
+
return unsubscribe;
|
|
12304
12156
|
}
|
|
12305
12157
|
|
|
12306
12158
|
// src/gateway-client.ts
|
|
@@ -12428,7 +12280,7 @@ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
|
12428
12280
|
clientId: "gateway-client",
|
|
12429
12281
|
clientMode: "backend",
|
|
12430
12282
|
role: "operator",
|
|
12431
|
-
scopes: ["operator.read", "operator.write"],
|
|
12283
|
+
scopes: ["operator.read", "operator.write", "operator.admin"],
|
|
12432
12284
|
signedAtMs,
|
|
12433
12285
|
token,
|
|
12434
12286
|
nonce,
|
|
@@ -12450,7 +12302,7 @@ function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
|
12450
12302
|
mode: "backend"
|
|
12451
12303
|
},
|
|
12452
12304
|
role: "operator",
|
|
12453
|
-
scopes: ["operator.read", "operator.write"],
|
|
12305
|
+
scopes: ["operator.read", "operator.write", "operator.admin"],
|
|
12454
12306
|
auth: { token },
|
|
12455
12307
|
device: {
|
|
12456
12308
|
id: identity.deviceId,
|
|
@@ -12487,13 +12339,7 @@ function parseHelloOk(response) {
|
|
|
12487
12339
|
snapshot: payload.snapshot
|
|
12488
12340
|
};
|
|
12489
12341
|
}
|
|
12490
|
-
|
|
12491
|
-
const g = globalThis;
|
|
12492
|
-
const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
|
|
12493
|
-
if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
|
|
12494
|
-
return hot.pendingGatewayRequests;
|
|
12495
|
-
}
|
|
12496
|
-
var GatewayClient2 = class {
|
|
12342
|
+
var GatewayClient = class {
|
|
12497
12343
|
port;
|
|
12498
12344
|
logger;
|
|
12499
12345
|
ws = null;
|
|
@@ -12507,9 +12353,12 @@ var GatewayClient2 = class {
|
|
|
12507
12353
|
tickIntervalMs = 15e3;
|
|
12508
12354
|
// default; overwritten by hello-ok response
|
|
12509
12355
|
deviceIdentity;
|
|
12356
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
12510
12357
|
/** Public Sets populated from hello-ok — consumers can inspect gateway capabilities */
|
|
12511
12358
|
availableMethods = /* @__PURE__ */ new Set();
|
|
12512
12359
|
availableEvents = /* @__PURE__ */ new Set();
|
|
12360
|
+
/** Called after a successful reconnection (WebSocket re-established after drop) */
|
|
12361
|
+
onReconnect = null;
|
|
12513
12362
|
/**
|
|
12514
12363
|
* @param port - Gateway WebSocket port
|
|
12515
12364
|
* @param token - Auth token (stored in closure via constructor param, NOT on this or globalThis)
|
|
@@ -12636,12 +12485,11 @@ var GatewayClient2 = class {
|
|
|
12636
12485
|
this.stopTickWatchdog();
|
|
12637
12486
|
diag("GW_CLIENT_WS_CLOSED", { port: this.port });
|
|
12638
12487
|
this.logger.warn("cohort-sync: gateway client WebSocket closed");
|
|
12639
|
-
const
|
|
12640
|
-
for (const [, entry] of pending) {
|
|
12488
|
+
for (const [, entry] of this.pendingRequests) {
|
|
12641
12489
|
clearTimeout(entry.timer);
|
|
12642
12490
|
entry.reject(new Error("Gateway WebSocket closed"));
|
|
12643
12491
|
}
|
|
12644
|
-
|
|
12492
|
+
this.pendingRequests.clear();
|
|
12645
12493
|
if (!settled) {
|
|
12646
12494
|
settle(new Error("Gateway WebSocket closed during handshake"));
|
|
12647
12495
|
}
|
|
@@ -12672,6 +12520,18 @@ var GatewayClient2 = class {
|
|
|
12672
12520
|
}
|
|
12673
12521
|
handlers.add(handler);
|
|
12674
12522
|
}
|
|
12523
|
+
/**
|
|
12524
|
+
* Remove an event handler previously registered with on().
|
|
12525
|
+
*/
|
|
12526
|
+
off(event, handler) {
|
|
12527
|
+
const handlers = this.eventHandlers.get(event);
|
|
12528
|
+
if (handlers) {
|
|
12529
|
+
handlers.delete(handler);
|
|
12530
|
+
if (handlers.size === 0) {
|
|
12531
|
+
this.eventHandlers.delete(event);
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12534
|
+
}
|
|
12675
12535
|
/**
|
|
12676
12536
|
* Send a request to the gateway and wait for the response.
|
|
12677
12537
|
*
|
|
@@ -12692,13 +12552,12 @@ var GatewayClient2 = class {
|
|
|
12692
12552
|
method,
|
|
12693
12553
|
params
|
|
12694
12554
|
};
|
|
12695
|
-
const pending = getPendingRequests();
|
|
12696
12555
|
return new Promise((resolve, reject) => {
|
|
12697
12556
|
const timer = setTimeout(() => {
|
|
12698
|
-
|
|
12557
|
+
this.pendingRequests.delete(id);
|
|
12699
12558
|
reject(new Error(`Gateway method "${method}" timed out after ${timeoutMs}ms`));
|
|
12700
12559
|
}, timeoutMs);
|
|
12701
|
-
|
|
12560
|
+
this.pendingRequests.set(id, {
|
|
12702
12561
|
resolve,
|
|
12703
12562
|
reject,
|
|
12704
12563
|
timer
|
|
@@ -12717,12 +12576,11 @@ var GatewayClient2 = class {
|
|
|
12717
12576
|
clearTimeout(this.reconnectTimer);
|
|
12718
12577
|
this.reconnectTimer = null;
|
|
12719
12578
|
}
|
|
12720
|
-
const
|
|
12721
|
-
for (const [, entry] of pending) {
|
|
12579
|
+
for (const [, entry] of this.pendingRequests) {
|
|
12722
12580
|
clearTimeout(entry.timer);
|
|
12723
12581
|
entry.reject(new Error("Gateway client closed"));
|
|
12724
12582
|
}
|
|
12725
|
-
|
|
12583
|
+
this.pendingRequests.clear();
|
|
12726
12584
|
if (this.ws) {
|
|
12727
12585
|
this.ws.close();
|
|
12728
12586
|
this.ws = null;
|
|
@@ -12752,10 +12610,9 @@ var GatewayClient2 = class {
|
|
|
12752
12610
|
}
|
|
12753
12611
|
}
|
|
12754
12612
|
handleResponse(frame) {
|
|
12755
|
-
const
|
|
12756
|
-
const entry = pending.get(frame.id);
|
|
12613
|
+
const entry = this.pendingRequests.get(frame.id);
|
|
12757
12614
|
if (!entry) return;
|
|
12758
|
-
|
|
12615
|
+
this.pendingRequests.delete(frame.id);
|
|
12759
12616
|
clearTimeout(entry.timer);
|
|
12760
12617
|
if (frame.ok) {
|
|
12761
12618
|
entry.resolve(frame.payload);
|
|
@@ -12830,6 +12687,7 @@ var GatewayClient2 = class {
|
|
|
12830
12687
|
diag("GW_CLIENT_RECONNECTING", { attempt: this.reconnectAttempts });
|
|
12831
12688
|
await this.connect();
|
|
12832
12689
|
diag("GW_CLIENT_RECONNECTED", { attempt: this.reconnectAttempts });
|
|
12690
|
+
this.onReconnect?.();
|
|
12833
12691
|
} catch (err) {
|
|
12834
12692
|
diag("GW_CLIENT_RECONNECT_FAILED", {
|
|
12835
12693
|
attempt: this.reconnectAttempts,
|
|
@@ -12840,6 +12698,94 @@ var GatewayClient2 = class {
|
|
|
12840
12698
|
}
|
|
12841
12699
|
};
|
|
12842
12700
|
|
|
12701
|
+
// src/micro-batch.ts
|
|
12702
|
+
var MicroBatch = class {
|
|
12703
|
+
buffer = [];
|
|
12704
|
+
timer = null;
|
|
12705
|
+
destroyed = false;
|
|
12706
|
+
maxSize;
|
|
12707
|
+
maxDelayMs;
|
|
12708
|
+
flushFn;
|
|
12709
|
+
onError;
|
|
12710
|
+
constructor(options) {
|
|
12711
|
+
this.maxSize = options.maxSize;
|
|
12712
|
+
this.maxDelayMs = options.maxDelayMs;
|
|
12713
|
+
this.flushFn = options.flush;
|
|
12714
|
+
this.onError = options.onError ?? ((err) => console.error("MicroBatch flush error:", err));
|
|
12715
|
+
}
|
|
12716
|
+
/**
|
|
12717
|
+
* Add an item to the batch.
|
|
12718
|
+
*
|
|
12719
|
+
* If the buffer was empty before this add, schedules an immediate flush
|
|
12720
|
+
* on the next tick (setTimeout 0). If the buffer already had items (burst),
|
|
12721
|
+
* starts a coalescing timer that fires after maxDelayMs.
|
|
12722
|
+
*
|
|
12723
|
+
* If the buffer reaches maxSize, flushes immediately.
|
|
12724
|
+
*/
|
|
12725
|
+
add(item) {
|
|
12726
|
+
if (this.destroyed) return;
|
|
12727
|
+
const wasEmpty = this.buffer.length === 0;
|
|
12728
|
+
this.buffer.push(item);
|
|
12729
|
+
if (this.buffer.length >= this.maxSize) {
|
|
12730
|
+
this.clearTimer();
|
|
12731
|
+
this.doFlush();
|
|
12732
|
+
return;
|
|
12733
|
+
}
|
|
12734
|
+
if (wasEmpty) {
|
|
12735
|
+
this.clearTimer();
|
|
12736
|
+
this.timer = setTimeout(() => {
|
|
12737
|
+
this.timer = null;
|
|
12738
|
+
this.doFlush();
|
|
12739
|
+
}, 0);
|
|
12740
|
+
} else if (!this.timer) {
|
|
12741
|
+
this.timer = setTimeout(() => {
|
|
12742
|
+
this.timer = null;
|
|
12743
|
+
this.doFlush();
|
|
12744
|
+
}, this.maxDelayMs);
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
/**
|
|
12748
|
+
* Flush all remaining items. Used for graceful shutdown.
|
|
12749
|
+
* No-op when buffer is empty.
|
|
12750
|
+
*/
|
|
12751
|
+
drain() {
|
|
12752
|
+
this.clearTimer();
|
|
12753
|
+
if (this.buffer.length > 0) {
|
|
12754
|
+
this.doFlush();
|
|
12755
|
+
}
|
|
12756
|
+
}
|
|
12757
|
+
/**
|
|
12758
|
+
* Clear any pending timer and discard buffered items.
|
|
12759
|
+
* The batch will not accept new items after destroy.
|
|
12760
|
+
*/
|
|
12761
|
+
destroy() {
|
|
12762
|
+
this.destroyed = true;
|
|
12763
|
+
this.clearTimer();
|
|
12764
|
+
this.buffer = [];
|
|
12765
|
+
}
|
|
12766
|
+
doFlush() {
|
|
12767
|
+
if (this.buffer.length === 0) return;
|
|
12768
|
+
const items = this.buffer;
|
|
12769
|
+
this.buffer = [];
|
|
12770
|
+
try {
|
|
12771
|
+
const result = this.flushFn(items);
|
|
12772
|
+
if (result && typeof result.catch === "function") {
|
|
12773
|
+
result.catch((err) => {
|
|
12774
|
+
this.onError(err);
|
|
12775
|
+
});
|
|
12776
|
+
}
|
|
12777
|
+
} catch (err) {
|
|
12778
|
+
this.onError(err);
|
|
12779
|
+
}
|
|
12780
|
+
}
|
|
12781
|
+
clearTimer() {
|
|
12782
|
+
if (this.timer !== null) {
|
|
12783
|
+
clearTimeout(this.timer);
|
|
12784
|
+
this.timer = null;
|
|
12785
|
+
}
|
|
12786
|
+
}
|
|
12787
|
+
};
|
|
12788
|
+
|
|
12843
12789
|
// src/agent-state.ts
|
|
12844
12790
|
import { basename } from "node:path";
|
|
12845
12791
|
|
|
@@ -13088,6 +13034,16 @@ function buildActivityEntry(agentName, hook, context) {
|
|
|
13088
13034
|
return null;
|
|
13089
13035
|
}
|
|
13090
13036
|
}
|
|
13037
|
+
var channelAgentBridge = /* @__PURE__ */ new Map();
|
|
13038
|
+
function setChannelAgent(channelId, agentName) {
|
|
13039
|
+
channelAgentBridge.set(channelId, agentName);
|
|
13040
|
+
}
|
|
13041
|
+
function getChannelAgent(channelId) {
|
|
13042
|
+
return channelAgentBridge.get(channelId) ?? null;
|
|
13043
|
+
}
|
|
13044
|
+
function getChannelAgentBridge() {
|
|
13045
|
+
return channelAgentBridge;
|
|
13046
|
+
}
|
|
13091
13047
|
var AgentStateTracker = class {
|
|
13092
13048
|
agents = /* @__PURE__ */ new Map();
|
|
13093
13049
|
activityBuffer = [];
|
|
@@ -13338,11 +13294,26 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
|
13338
13294
|
`;
|
|
13339
13295
|
|
|
13340
13296
|
// src/hooks.ts
|
|
13341
|
-
var
|
|
13297
|
+
var REDACT_KEYS = /* @__PURE__ */ new Set([
|
|
13298
|
+
"token",
|
|
13299
|
+
"apikey",
|
|
13300
|
+
"secret",
|
|
13301
|
+
"password",
|
|
13302
|
+
"credential",
|
|
13303
|
+
"authorization",
|
|
13304
|
+
"accesstoken",
|
|
13305
|
+
"refreshtoken",
|
|
13306
|
+
"bearer",
|
|
13307
|
+
"privatekey"
|
|
13308
|
+
]);
|
|
13342
13309
|
function dumpCtx(ctx) {
|
|
13343
13310
|
if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
|
|
13344
13311
|
const out = {};
|
|
13345
13312
|
for (const key of Object.keys(ctx)) {
|
|
13313
|
+
if (REDACT_KEYS.has(key.toLowerCase())) {
|
|
13314
|
+
out[key] = "[REDACTED]";
|
|
13315
|
+
continue;
|
|
13316
|
+
}
|
|
13346
13317
|
const val = ctx[key];
|
|
13347
13318
|
if (typeof val === "function") {
|
|
13348
13319
|
out[key] = "[Function]";
|
|
@@ -13368,8 +13339,7 @@ try {
|
|
|
13368
13339
|
PLUGIN_VERSION = pkgJson.version ?? "unknown";
|
|
13369
13340
|
} catch {
|
|
13370
13341
|
}
|
|
13371
|
-
diag("MODULE_LOADED", {
|
|
13372
|
-
var lastCronSnapshotJson = "";
|
|
13342
|
+
diag("MODULE_LOADED", { PLUGIN_VERSION });
|
|
13373
13343
|
function resolveGatewayToken(api) {
|
|
13374
13344
|
const rawToken = api.config?.gateway?.auth?.token;
|
|
13375
13345
|
if (typeof rawToken === "string") return rawToken;
|
|
@@ -13378,26 +13348,6 @@ function resolveGatewayToken(api) {
|
|
|
13378
13348
|
}
|
|
13379
13349
|
return null;
|
|
13380
13350
|
}
|
|
13381
|
-
async function quickCronSync(port, token, cfg, resolveAgentName, logger) {
|
|
13382
|
-
const client2 = new GatewayClient2(port, token, logger, PLUGIN_VERSION);
|
|
13383
|
-
try {
|
|
13384
|
-
await client2.connect();
|
|
13385
|
-
const result = await client2.request("cron.list", { includeDisabled: true });
|
|
13386
|
-
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 });
|
|
13387
|
-
const jobs = Array.isArray(result) ? result : result?.jobs ?? [];
|
|
13388
|
-
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13389
|
-
const serialized = JSON.stringify(mapped);
|
|
13390
|
-
if (serialized !== lastCronSnapshotJson) {
|
|
13391
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13392
|
-
lastCronSnapshotJson = serialized;
|
|
13393
|
-
diag("HEARTBEAT_CRON_PUSHED", { count: mapped.length });
|
|
13394
|
-
} else {
|
|
13395
|
-
diag("HEARTBEAT_CRON_UNCHANGED", {});
|
|
13396
|
-
}
|
|
13397
|
-
} finally {
|
|
13398
|
-
client2.close();
|
|
13399
|
-
}
|
|
13400
|
-
}
|
|
13401
13351
|
function registerCronEventHandlers(client2, cfg, resolveAgentName) {
|
|
13402
13352
|
if (client2.availableEvents.has("cron")) {
|
|
13403
13353
|
let debounceTimer = null;
|
|
@@ -13449,23 +13399,25 @@ function resolveIdentity(configIdentity, workspaceDir) {
|
|
|
13449
13399
|
avatar: configIdentity?.avatar ?? fileIdentity?.avatar
|
|
13450
13400
|
};
|
|
13451
13401
|
}
|
|
13402
|
+
var tracker = null;
|
|
13452
13403
|
function getOrCreateTracker() {
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13404
|
+
if (!tracker) tracker = new AgentStateTracker();
|
|
13405
|
+
return tracker;
|
|
13406
|
+
}
|
|
13407
|
+
var gatewayPort = null;
|
|
13408
|
+
var gatewayToken = null;
|
|
13409
|
+
var persistentGwClient = null;
|
|
13410
|
+
var gwClientInitialized = false;
|
|
13411
|
+
var keepaliveInterval = null;
|
|
13412
|
+
var commandUnsubscriber2 = null;
|
|
13461
13413
|
var STATE_FILE_PATH = "";
|
|
13462
|
-
function saveSessionsToDisk(
|
|
13414
|
+
function saveSessionsToDisk(tracker2) {
|
|
13463
13415
|
try {
|
|
13464
|
-
const state =
|
|
13416
|
+
const state = tracker2.exportState();
|
|
13465
13417
|
const data = {
|
|
13466
13418
|
sessions: [],
|
|
13467
13419
|
sessionKeyToAgent: Object.fromEntries(state.sessionKeyToAgent),
|
|
13468
|
-
channelAgents:
|
|
13420
|
+
channelAgents: Object.fromEntries(getChannelAgentBridge()),
|
|
13469
13421
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13470
13422
|
};
|
|
13471
13423
|
for (const [name, agent] of state.agents) {
|
|
@@ -13477,7 +13429,7 @@ function saveSessionsToDisk(tracker) {
|
|
|
13477
13429
|
} catch {
|
|
13478
13430
|
}
|
|
13479
13431
|
}
|
|
13480
|
-
function loadSessionsFromDisk(
|
|
13432
|
+
function loadSessionsFromDisk(tracker2, logger) {
|
|
13481
13433
|
try {
|
|
13482
13434
|
if (!fs3.existsSync(STATE_FILE_PATH)) return;
|
|
13483
13435
|
const data = JSON.parse(fs3.readFileSync(STATE_FILE_PATH, "utf8"));
|
|
@@ -13487,13 +13439,13 @@ function loadSessionsFromDisk(tracker, logger) {
|
|
|
13487
13439
|
}
|
|
13488
13440
|
let count = 0;
|
|
13489
13441
|
for (const { agentName, key } of data.sessions) {
|
|
13490
|
-
if (!
|
|
13491
|
-
|
|
13442
|
+
if (!tracker2.hasSession(agentName, key)) {
|
|
13443
|
+
tracker2.addSession(agentName, key);
|
|
13492
13444
|
count++;
|
|
13493
13445
|
}
|
|
13494
13446
|
}
|
|
13495
13447
|
for (const [key, agent] of Object.entries(data.sessionKeyToAgent)) {
|
|
13496
|
-
|
|
13448
|
+
tracker2.setSessionAgent(key, agent);
|
|
13497
13449
|
}
|
|
13498
13450
|
for (const [channelId, agent] of Object.entries(data.channelAgents ?? {})) {
|
|
13499
13451
|
setChannelAgent(channelId, agent);
|
|
@@ -13521,16 +13473,52 @@ async function fetchAgentContext(apiKey, apiUrl, logger) {
|
|
|
13521
13473
|
return POCKET_GUIDE;
|
|
13522
13474
|
}
|
|
13523
13475
|
}
|
|
13476
|
+
function initGatewayClient(port, token, cfg, resolveAgentName, logger) {
|
|
13477
|
+
const client2 = new GatewayClient(port, token, logger, PLUGIN_VERSION);
|
|
13478
|
+
persistentGwClient = client2;
|
|
13479
|
+
gwClientInitialized = true;
|
|
13480
|
+
const onConnected = async () => {
|
|
13481
|
+
diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
|
|
13482
|
+
logger.info(`cohort-sync: gateway client connected (methods=${client2.availableMethods.size}, events=${client2.availableEvents.size})`);
|
|
13483
|
+
registerCronEventHandlers(client2, cfg, resolveAgentName);
|
|
13484
|
+
if (client2.availableEvents.has("shutdown")) {
|
|
13485
|
+
client2.on("shutdown", () => {
|
|
13486
|
+
diag("GW_CLIENT_SHUTDOWN_EVENT", {});
|
|
13487
|
+
logger.info("cohort-sync: gateway shutdown event received");
|
|
13488
|
+
});
|
|
13489
|
+
}
|
|
13490
|
+
try {
|
|
13491
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13492
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13493
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13494
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13495
|
+
diag("GW_CLIENT_CRON_PUSH", { count: mapped.length });
|
|
13496
|
+
} catch (err) {
|
|
13497
|
+
diag("GW_CLIENT_CRON_PUSH_FAILED", { error: String(err) });
|
|
13498
|
+
}
|
|
13499
|
+
};
|
|
13500
|
+
client2.onReconnect = onConnected;
|
|
13501
|
+
client2.connect().then(() => onConnected()).catch((err) => {
|
|
13502
|
+
diag("GW_CLIENT_INITIAL_CONNECT_DEFERRED", { error: String(err) });
|
|
13503
|
+
logger.warn(`cohort-sync: GW connect will retry: ${String(err)}`);
|
|
13504
|
+
});
|
|
13505
|
+
}
|
|
13524
13506
|
function registerHooks(api, cfg) {
|
|
13525
13507
|
STATE_FILE_PATH = path3.join(cfg.stateDir, "session-state.json");
|
|
13526
13508
|
const { logger, config } = api;
|
|
13527
13509
|
const nameMap = cfg.agentNameMap;
|
|
13528
|
-
const
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
logger
|
|
13510
|
+
const tracker2 = getOrCreateTracker();
|
|
13511
|
+
const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
|
|
13512
|
+
createClient(convexUrl);
|
|
13513
|
+
setLogger(logger);
|
|
13514
|
+
gatewayPort = api.config?.gateway?.port ?? null;
|
|
13515
|
+
gatewayToken = resolveGatewayToken(api);
|
|
13516
|
+
if (gatewayPort && gatewayToken) {
|
|
13517
|
+
initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
|
|
13518
|
+
}
|
|
13519
|
+
const cronStorePath = api.config?.cron?.store ?? path3.join(os3.homedir(), ".openclaw", "cron", "jobs.json");
|
|
13520
|
+
logger.info(`cohort-sync: registerHooks v${PLUGIN_VERSION}`);
|
|
13532
13521
|
diag("REGISTER_HOOKS", {
|
|
13533
|
-
BUILD_ID,
|
|
13534
13522
|
PLUGIN_VERSION,
|
|
13535
13523
|
hasNameMap: !!nameMap,
|
|
13536
13524
|
nameMapKeys: nameMap ? Object.keys(nameMap) : [],
|
|
@@ -13539,10 +13527,6 @@ function registerHooks(api, cfg) {
|
|
|
13539
13527
|
agentIds: (config?.agents?.list ?? []).map((a) => a.id),
|
|
13540
13528
|
agentMessageProviders: (config?.agents?.list ?? []).map((a) => ({ id: a.id, mp: a.messageProvider }))
|
|
13541
13529
|
});
|
|
13542
|
-
setConvexUrl(cfg);
|
|
13543
|
-
setLogger(logger);
|
|
13544
|
-
restoreFromHotReload(logger);
|
|
13545
|
-
restoreRosterFromHotReload(getRosterHotState(), logger);
|
|
13546
13530
|
const identityNameMap = {};
|
|
13547
13531
|
const mainIdentity = parseIdentityFile(process.cwd());
|
|
13548
13532
|
if (mainIdentity?.name) {
|
|
@@ -13555,14 +13539,14 @@ function registerHooks(api, cfg) {
|
|
|
13555
13539
|
}
|
|
13556
13540
|
}
|
|
13557
13541
|
diag("IDENTITY_NAME_MAP", { identityNameMap });
|
|
13558
|
-
if (
|
|
13559
|
-
loadSessionsFromDisk(
|
|
13560
|
-
const restoredAgents =
|
|
13542
|
+
if (tracker2.getAgentNames().length === 0) {
|
|
13543
|
+
loadSessionsFromDisk(tracker2, logger);
|
|
13544
|
+
const restoredAgents = tracker2.getAgentNames();
|
|
13561
13545
|
for (const agentName of restoredAgents) {
|
|
13562
|
-
const sessSnapshot =
|
|
13546
|
+
const sessSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
13563
13547
|
if (sessSnapshot.length > 0) {
|
|
13564
13548
|
pushSessions(cfg.apiKey, agentName, sessSnapshot).then(() => {
|
|
13565
|
-
|
|
13549
|
+
tracker2.markSessionsPushed(agentName);
|
|
13566
13550
|
logger.info(`cohort-sync: pushed ${sessSnapshot.length} restored sessions for ${agentName}`);
|
|
13567
13551
|
}).catch((err) => {
|
|
13568
13552
|
logger.warn(`cohort-sync: failed to push restored sessions for ${agentName}: ${String(err)}`);
|
|
@@ -13593,7 +13577,7 @@ function registerHooks(api, cfg) {
|
|
|
13593
13577
|
}
|
|
13594
13578
|
const sessionKey = ctx.sessionKey ?? ctx.sessionId;
|
|
13595
13579
|
if (sessionKey && typeof sessionKey === "string") {
|
|
13596
|
-
const mapped =
|
|
13580
|
+
const mapped = tracker2.getSessionAgent(sessionKey);
|
|
13597
13581
|
if (mapped) {
|
|
13598
13582
|
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "sessionKey_mapped", sessionKey, mapped });
|
|
13599
13583
|
return mapped;
|
|
@@ -13615,10 +13599,10 @@ function registerHooks(api, cfg) {
|
|
|
13615
13599
|
if (channelId && typeof channelId === "string") {
|
|
13616
13600
|
const channelAgent = getChannelAgent(channelId);
|
|
13617
13601
|
if (channelAgent) {
|
|
13618
|
-
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_bridge", channelId, channelAgent, bridgeState:
|
|
13602
|
+
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_bridge", channelId, channelAgent, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
|
|
13619
13603
|
return channelAgent;
|
|
13620
13604
|
}
|
|
13621
|
-
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_raw", channelId, bridgeState:
|
|
13605
|
+
diag("RESOLVE_AGENT_FROM_CTX_RESULT", { method: "channelId_raw", channelId, bridgeState: Object.fromEntries(getChannelAgentBridge()) });
|
|
13622
13606
|
return String(channelId);
|
|
13623
13607
|
}
|
|
13624
13608
|
const resolved = resolveAgentName("main");
|
|
@@ -13639,48 +13623,20 @@ function registerHooks(api, cfg) {
|
|
|
13639
13623
|
}
|
|
13640
13624
|
return "unknown";
|
|
13641
13625
|
}
|
|
13642
|
-
const
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
})
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
clearIntervalsFromHot();
|
|
13650
|
-
const heartbeatMs = cfg.syncIntervalMs ?? 12e4;
|
|
13651
|
-
heartbeatInterval = setInterval(() => {
|
|
13652
|
-
pushHeartbeat().catch((err) => {
|
|
13653
|
-
logger.warn(`cohort-sync: heartbeat tick failed: ${String(err)}`);
|
|
13654
|
-
});
|
|
13655
|
-
}, heartbeatMs);
|
|
13656
|
-
activityFlushInterval = setInterval(() => {
|
|
13657
|
-
flushActivityBuffer().catch((err) => {
|
|
13658
|
-
logger.warn(`cohort-sync: activity flush tick failed: ${String(err)}`);
|
|
13659
|
-
});
|
|
13660
|
-
}, 3e3);
|
|
13661
|
-
saveIntervalsToHot(heartbeatInterval, activityFlushInterval);
|
|
13662
|
-
logger.info(`cohort-sync: intervals created (heartbeat=${heartbeatMs / 1e3}s, activityFlush=3s)`);
|
|
13663
|
-
{
|
|
13664
|
-
const hotState = getHotState();
|
|
13665
|
-
if (hotState.commandSubscription) {
|
|
13666
|
-
hotState.commandSubscription();
|
|
13667
|
-
hotState.commandSubscription = null;
|
|
13668
|
-
}
|
|
13669
|
-
const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
|
|
13670
|
-
hotState.commandSubscription = unsub;
|
|
13671
|
-
}
|
|
13626
|
+
const activityBatch = new MicroBatch({
|
|
13627
|
+
maxSize: 10,
|
|
13628
|
+
maxDelayMs: 1e3,
|
|
13629
|
+
flush: (entries) => pushActivity(cfg.apiKey, entries),
|
|
13630
|
+
onError: (err) => logger.warn(`cohort-sync: activity batch flush failed: ${String(err)}`)
|
|
13631
|
+
});
|
|
13632
|
+
const KEEPALIVE_INTERVAL_MS = 15e4;
|
|
13672
13633
|
{
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
const hotState = getHotState();
|
|
13677
|
-
hotState.gatewayPort = port;
|
|
13678
|
-
hotState.gatewayToken = token;
|
|
13679
|
-
diag("REGISTER_HOOKS_CRON_SYNC", { port });
|
|
13680
|
-
quickCronSync(port, token, cfg, resolveAgentName, logger).catch((err) => {
|
|
13681
|
-
diag("REGISTER_HOOKS_CRON_SYNC_FAILED", { error: String(err) });
|
|
13682
|
-
});
|
|
13634
|
+
if (commandUnsubscriber2) {
|
|
13635
|
+
commandUnsubscriber2();
|
|
13636
|
+
commandUnsubscriber2 = null;
|
|
13683
13637
|
}
|
|
13638
|
+
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13639
|
+
commandUnsubscriber2 = unsub;
|
|
13684
13640
|
}
|
|
13685
13641
|
api.registerTool((toolCtx) => {
|
|
13686
13642
|
const agentId = toolCtx.agentId ?? "main";
|
|
@@ -13796,52 +13752,6 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13796
13752
|
if (m.includes("deepseek")) return 128e3;
|
|
13797
13753
|
return 2e5;
|
|
13798
13754
|
}
|
|
13799
|
-
async function pushHeartbeat() {
|
|
13800
|
-
const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
|
|
13801
|
-
for (const agentId of allAgentIds) {
|
|
13802
|
-
const agentName = resolveAgentName(agentId);
|
|
13803
|
-
const pruned = tracker.pruneStaleSessions(agentName, 864e5);
|
|
13804
|
-
if (pruned.length > 0) {
|
|
13805
|
-
logger.info(`cohort-sync: pruned ${pruned.length} stale sessions for ${agentName}`);
|
|
13806
|
-
}
|
|
13807
|
-
}
|
|
13808
|
-
for (const agentId of allAgentIds) {
|
|
13809
|
-
const agentName = resolveAgentName(agentId);
|
|
13810
|
-
try {
|
|
13811
|
-
const snapshot = tracker.getTelemetrySnapshot(agentName);
|
|
13812
|
-
if (snapshot) {
|
|
13813
|
-
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13814
|
-
} else {
|
|
13815
|
-
logger.info(`cohort-sync: heartbeat skipped ${agentName} \u2014 no snapshot in tracker`);
|
|
13816
|
-
}
|
|
13817
|
-
} catch (err) {
|
|
13818
|
-
logger.warn(`cohort-sync: heartbeat push failed for ${agentName}: ${String(err)}`);
|
|
13819
|
-
}
|
|
13820
|
-
}
|
|
13821
|
-
for (const agentId of allAgentIds) {
|
|
13822
|
-
const agentName = resolveAgentName(agentId);
|
|
13823
|
-
try {
|
|
13824
|
-
if (tracker.shouldPushSessions(agentName)) {
|
|
13825
|
-
const sessSnapshot = tracker.getSessionsSnapshot(agentName);
|
|
13826
|
-
await pushSessions(cfg.apiKey, agentName, sessSnapshot);
|
|
13827
|
-
tracker.markSessionsPushed(agentName);
|
|
13828
|
-
}
|
|
13829
|
-
} catch (err) {
|
|
13830
|
-
logger.warn(`cohort-sync: heartbeat session push failed for ${agentName}: ${String(err)}`);
|
|
13831
|
-
}
|
|
13832
|
-
}
|
|
13833
|
-
saveSessionsToDisk(tracker);
|
|
13834
|
-
logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
|
|
13835
|
-
}
|
|
13836
|
-
async function flushActivityBuffer() {
|
|
13837
|
-
const entries = drainActivityFromHot();
|
|
13838
|
-
if (entries.length === 0) return;
|
|
13839
|
-
try {
|
|
13840
|
-
await pushActivity(cfg.apiKey, entries);
|
|
13841
|
-
} catch (err) {
|
|
13842
|
-
logger.warn(`cohort-sync: activity flush failed: ${String(err)}`);
|
|
13843
|
-
}
|
|
13844
|
-
}
|
|
13845
13755
|
api.on("gateway_start", async (event) => {
|
|
13846
13756
|
diag("HOOK_gateway_start", { port: event.port, eventKeys: Object.keys(event) });
|
|
13847
13757
|
try {
|
|
@@ -13867,24 +13777,20 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13867
13777
|
setChannelAgent(mp, agentName);
|
|
13868
13778
|
}
|
|
13869
13779
|
}
|
|
13870
|
-
diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge:
|
|
13871
|
-
|
|
13872
|
-
hotState.gatewayPort = event.port;
|
|
13780
|
+
diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: Object.fromEntries(getChannelAgentBridge()) });
|
|
13781
|
+
gatewayPort = event.port;
|
|
13873
13782
|
const token = resolveGatewayToken(api);
|
|
13874
13783
|
if (token) {
|
|
13784
|
+
gatewayToken = token;
|
|
13875
13785
|
diag("GW_CLIENT_CONNECTING", { port: event.port, hasToken: true });
|
|
13876
13786
|
try {
|
|
13877
|
-
|
|
13878
|
-
|
|
13879
|
-
|
|
13880
|
-
|
|
13881
|
-
|
|
13882
|
-
const
|
|
13883
|
-
|
|
13884
|
-
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13885
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13886
|
-
lastCronSnapshotJson = JSON.stringify(mapped);
|
|
13887
|
-
diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
|
|
13787
|
+
await initGatewayClient(event.port, token, cfg, resolveAgentName, logger);
|
|
13788
|
+
if (commandUnsubscriber2) {
|
|
13789
|
+
commandUnsubscriber2();
|
|
13790
|
+
commandUnsubscriber2 = null;
|
|
13791
|
+
}
|
|
13792
|
+
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13793
|
+
commandUnsubscriber2 = unsub;
|
|
13888
13794
|
} catch (err) {
|
|
13889
13795
|
diag("GW_CLIENT_CONNECT_FAILED", { error: String(err) });
|
|
13890
13796
|
logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
|
|
@@ -13893,7 +13799,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13893
13799
|
diag("GW_CLIENT_NO_TOKEN", {});
|
|
13894
13800
|
logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
|
|
13895
13801
|
}
|
|
13896
|
-
await
|
|
13802
|
+
await startNotificationSubscription(
|
|
13897
13803
|
event.port,
|
|
13898
13804
|
cfg,
|
|
13899
13805
|
api.config.hooks?.token,
|
|
@@ -13905,31 +13811,61 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13905
13811
|
for (const agentId of allAgentIds) {
|
|
13906
13812
|
const agentName = resolveAgentName(agentId);
|
|
13907
13813
|
try {
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
const snapshot =
|
|
13814
|
+
tracker2.setModel(agentName, resolveModel(agentId));
|
|
13815
|
+
tracker2.updateStatus(agentName, "idle");
|
|
13816
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13911
13817
|
if (snapshot) {
|
|
13912
13818
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13913
|
-
|
|
13819
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13914
13820
|
}
|
|
13915
13821
|
} catch (err) {
|
|
13916
13822
|
logger.warn(`cohort-sync: initial telemetry seed failed for ${agentName}: ${String(err)}`);
|
|
13917
13823
|
}
|
|
13918
13824
|
}
|
|
13919
13825
|
logger.info(`cohort-sync: seeded telemetry for ${allAgentIds.length} agents`);
|
|
13826
|
+
if (keepaliveInterval) clearInterval(keepaliveInterval);
|
|
13827
|
+
keepaliveInterval = setInterval(async () => {
|
|
13828
|
+
for (const agentId of allAgentIds) {
|
|
13829
|
+
const agentName = resolveAgentName(agentId);
|
|
13830
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13831
|
+
if (snapshot) {
|
|
13832
|
+
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION }).catch(() => {
|
|
13833
|
+
});
|
|
13834
|
+
}
|
|
13835
|
+
}
|
|
13836
|
+
for (const agentId of allAgentIds) {
|
|
13837
|
+
const agentName = resolveAgentName(agentId);
|
|
13838
|
+
tracker2.pruneStaleSessions(agentName, 864e5);
|
|
13839
|
+
}
|
|
13840
|
+
saveSessionsToDisk(tracker2);
|
|
13841
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
13842
|
+
logger.info(`cohort-sync: keepalive interval started (${KEEPALIVE_INTERVAL_MS / 1e3}s)`);
|
|
13920
13843
|
});
|
|
13921
13844
|
api.on("agent_end", async (event, ctx) => {
|
|
13922
13845
|
diag("HOOK_agent_end", { ctx: dumpCtx(ctx), success: event.success, error: event.error, durationMs: event.durationMs });
|
|
13923
13846
|
const agentId = ctx.agentId ?? "main";
|
|
13924
13847
|
const agentName = resolveAgentName(agentId);
|
|
13925
13848
|
try {
|
|
13926
|
-
|
|
13849
|
+
tracker2.updateStatus(agentName, "idle");
|
|
13927
13850
|
await syncAgentStatus(agentName, "idle", resolveModel(agentId), cfg, logger);
|
|
13928
|
-
if (
|
|
13929
|
-
const snapshot =
|
|
13851
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13852
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13930
13853
|
if (snapshot) {
|
|
13931
13854
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13932
|
-
|
|
13855
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13856
|
+
}
|
|
13857
|
+
}
|
|
13858
|
+
const sessionKey = ctx.sessionKey;
|
|
13859
|
+
if (sessionKey && sessionKey.includes(":cron:")) {
|
|
13860
|
+
try {
|
|
13861
|
+
const raw = fs3.readFileSync(cronStorePath, "utf8");
|
|
13862
|
+
const store = JSON.parse(raw);
|
|
13863
|
+
const jobs = store.jobs ?? [];
|
|
13864
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13865
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13866
|
+
diag("CRON_AGENT_END_PUSH", { count: mapped.length, sessionKey });
|
|
13867
|
+
} catch (err) {
|
|
13868
|
+
diag("CRON_AGENT_END_PUSH_FAILED", { error: String(err) });
|
|
13933
13869
|
}
|
|
13934
13870
|
}
|
|
13935
13871
|
if (event.success === false) {
|
|
@@ -13939,7 +13875,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13939
13875
|
durationMs: event.durationMs,
|
|
13940
13876
|
sessionKey: ctx.sessionKey
|
|
13941
13877
|
});
|
|
13942
|
-
if (entry)
|
|
13878
|
+
if (entry) activityBatch.add(entry);
|
|
13943
13879
|
}
|
|
13944
13880
|
} catch (err) {
|
|
13945
13881
|
logger.warn(`cohort-sync: agent_end sync failed: ${String(err)}`);
|
|
@@ -13964,31 +13900,31 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13964
13900
|
const agentName = resolveAgentName(agentId);
|
|
13965
13901
|
try {
|
|
13966
13902
|
const sessionKey = ctx.sessionKey;
|
|
13967
|
-
|
|
13903
|
+
tracker2.updateFromLlmOutput(agentName, sessionKey, {
|
|
13968
13904
|
model,
|
|
13969
13905
|
tokensIn: usage.input ?? 0,
|
|
13970
13906
|
tokensOut: usage.output ?? 0,
|
|
13971
13907
|
contextTokens,
|
|
13972
13908
|
contextLimit
|
|
13973
13909
|
});
|
|
13974
|
-
if (sessionKey && !
|
|
13975
|
-
|
|
13910
|
+
if (sessionKey && !tracker2.hasSession(agentName, sessionKey)) {
|
|
13911
|
+
tracker2.addSession(agentName, sessionKey);
|
|
13976
13912
|
logger.info(`cohort-sync: inferred session for ${agentName} from llm_output (${sessionKey})`);
|
|
13977
13913
|
}
|
|
13978
13914
|
if (sessionKey) {
|
|
13979
|
-
|
|
13915
|
+
tracker2.setSessionAgent(sessionKey, agentName);
|
|
13980
13916
|
}
|
|
13981
|
-
if (
|
|
13982
|
-
const snapshot =
|
|
13917
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13918
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
13983
13919
|
if (snapshot) {
|
|
13984
13920
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
13985
|
-
|
|
13921
|
+
tracker2.markTelemetryPushed(agentName);
|
|
13986
13922
|
}
|
|
13987
13923
|
}
|
|
13988
|
-
if (
|
|
13989
|
-
const sessionsSnapshot =
|
|
13924
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
13925
|
+
const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
13990
13926
|
await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
|
|
13991
|
-
|
|
13927
|
+
tracker2.markSessionsPushed(agentName);
|
|
13992
13928
|
}
|
|
13993
13929
|
} catch (err) {
|
|
13994
13930
|
logger.warn(`cohort-sync: llm_output telemetry failed: ${String(err)}`);
|
|
@@ -13999,15 +13935,15 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13999
13935
|
const agentId = ctx.agentId ?? "main";
|
|
14000
13936
|
const agentName = resolveAgentName(agentId);
|
|
14001
13937
|
try {
|
|
14002
|
-
|
|
13938
|
+
tracker2.updateFromCompaction(agentName, {
|
|
14003
13939
|
contextTokens: event.tokenCount ?? 0,
|
|
14004
13940
|
contextLimit: getModelContextLimit(resolveModel(agentId))
|
|
14005
13941
|
});
|
|
14006
|
-
if (
|
|
14007
|
-
const snapshot =
|
|
13942
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13943
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
14008
13944
|
if (snapshot) {
|
|
14009
13945
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
14010
|
-
|
|
13946
|
+
tracker2.markTelemetryPushed(agentName);
|
|
14011
13947
|
}
|
|
14012
13948
|
}
|
|
14013
13949
|
const entry = buildActivityEntry(agentName, "after_compaction", {
|
|
@@ -14015,7 +13951,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14015
13951
|
compactedCount: event.compactedCount,
|
|
14016
13952
|
sessionKey: ctx.sessionKey
|
|
14017
13953
|
});
|
|
14018
|
-
if (entry)
|
|
13954
|
+
if (entry) activityBatch.add(entry);
|
|
14019
13955
|
} catch (err) {
|
|
14020
13956
|
logger.warn(`cohort-sync: after_compaction telemetry failed: ${String(err)}`);
|
|
14021
13957
|
}
|
|
@@ -14026,26 +13962,39 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14026
13962
|
const agentName = resolveAgentName(agentId);
|
|
14027
13963
|
diag("HOOK_before_agent_start_RESOLVED", { agentId, agentName, ctxChannelId: ctx.channelId, ctxMessageProvider: ctx.messageProvider, ctxSessionKey: ctx.sessionKey, ctxAccountId: ctx.accountId });
|
|
14028
13964
|
try {
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
13965
|
+
if (!gwClientInitialized && gatewayPort && gatewayToken) {
|
|
13966
|
+
try {
|
|
13967
|
+
await initGatewayClient(gatewayPort, gatewayToken, cfg, resolveAgentName, logger);
|
|
13968
|
+
if (commandUnsubscriber2) {
|
|
13969
|
+
commandUnsubscriber2();
|
|
13970
|
+
commandUnsubscriber2 = null;
|
|
13971
|
+
}
|
|
13972
|
+
const unsub = startCommandSubscription(cfg, logger, resolveAgentName, persistentGwClient);
|
|
13973
|
+
commandUnsubscriber2 = unsub;
|
|
13974
|
+
} catch (err) {
|
|
13975
|
+
diag("GW_CLIENT_LAZY_INIT_FAILED", { error: String(err) });
|
|
13976
|
+
}
|
|
13977
|
+
}
|
|
13978
|
+
tracker2.updateStatus(agentName, "working");
|
|
13979
|
+
if (tracker2.shouldPushTelemetry(agentName)) {
|
|
13980
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
14032
13981
|
if (snapshot) {
|
|
14033
13982
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
14034
|
-
|
|
13983
|
+
tracker2.markTelemetryPushed(agentName);
|
|
14035
13984
|
}
|
|
14036
13985
|
}
|
|
14037
13986
|
const sessionKey = ctx.sessionKey;
|
|
14038
|
-
if (sessionKey && !
|
|
14039
|
-
|
|
14040
|
-
|
|
13987
|
+
if (sessionKey && !tracker2.hasSession(agentName, sessionKey)) {
|
|
13988
|
+
tracker2.addSession(agentName, sessionKey);
|
|
13989
|
+
tracker2.setSessionAgent(sessionKey, agentName);
|
|
14041
13990
|
logger.info(`cohort-sync: inferred session for ${agentName} (${sessionKey})`);
|
|
14042
|
-
if (
|
|
14043
|
-
const sessSnapshot =
|
|
13991
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
13992
|
+
const sessSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
14044
13993
|
await pushSessions(cfg.apiKey, agentName, sessSnapshot);
|
|
14045
|
-
|
|
13994
|
+
tracker2.markSessionsPushed(agentName);
|
|
14046
13995
|
}
|
|
14047
13996
|
} else if (sessionKey) {
|
|
14048
|
-
|
|
13997
|
+
tracker2.setSessionAgent(sessionKey, agentName);
|
|
14049
13998
|
}
|
|
14050
13999
|
const ctxChannelId = ctx.channelId;
|
|
14051
14000
|
if (ctxChannelId) {
|
|
@@ -14065,11 +14014,11 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14065
14014
|
const agentName = resolveAgentName(agentId);
|
|
14066
14015
|
try {
|
|
14067
14016
|
const sessionKey = ctx.sessionId ?? String(Date.now());
|
|
14068
|
-
|
|
14069
|
-
if (
|
|
14070
|
-
const sessionsSnapshot =
|
|
14017
|
+
tracker2.addSession(agentName, sessionKey);
|
|
14018
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
14019
|
+
const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
14071
14020
|
await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
|
|
14072
|
-
|
|
14021
|
+
tracker2.markSessionsPushed(agentName);
|
|
14073
14022
|
}
|
|
14074
14023
|
const parsed = parseSessionKey(sessionKey);
|
|
14075
14024
|
const entry = buildActivityEntry(agentName, "session_start", {
|
|
@@ -14077,7 +14026,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14077
14026
|
sessionKey,
|
|
14078
14027
|
resumedFrom: event.resumedFrom
|
|
14079
14028
|
});
|
|
14080
|
-
if (entry)
|
|
14029
|
+
if (entry) activityBatch.add(entry);
|
|
14081
14030
|
} catch (err) {
|
|
14082
14031
|
logger.warn(`cohort-sync: session_start tracking failed: ${String(err)}`);
|
|
14083
14032
|
}
|
|
@@ -14088,18 +14037,18 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14088
14037
|
const agentName = resolveAgentName(agentId);
|
|
14089
14038
|
try {
|
|
14090
14039
|
const sessionKey = ctx.sessionId ?? "";
|
|
14091
|
-
|
|
14092
|
-
if (
|
|
14093
|
-
const sessionsSnapshot =
|
|
14040
|
+
tracker2.removeSession(agentName, sessionKey);
|
|
14041
|
+
if (tracker2.shouldPushSessions(agentName)) {
|
|
14042
|
+
const sessionsSnapshot = tracker2.getSessionsSnapshot(agentName);
|
|
14094
14043
|
await pushSessions(cfg.apiKey, agentName, sessionsSnapshot);
|
|
14095
|
-
|
|
14044
|
+
tracker2.markSessionsPushed(agentName);
|
|
14096
14045
|
}
|
|
14097
14046
|
const entry = buildActivityEntry(agentName, "session_end", {
|
|
14098
14047
|
sessionKey,
|
|
14099
14048
|
messageCount: event.messageCount,
|
|
14100
14049
|
durationMs: event.durationMs
|
|
14101
14050
|
});
|
|
14102
|
-
if (entry)
|
|
14051
|
+
if (entry) activityBatch.add(entry);
|
|
14103
14052
|
} catch (err) {
|
|
14104
14053
|
logger.warn(`cohort-sync: session_end tracking failed: ${String(err)}`);
|
|
14105
14054
|
}
|
|
@@ -14116,7 +14065,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14116
14065
|
sessionKey: ctx.sessionKey,
|
|
14117
14066
|
model: resolveModel(ctx.agentId ?? "main")
|
|
14118
14067
|
});
|
|
14119
|
-
if (entry)
|
|
14068
|
+
if (entry) activityBatch.add(entry);
|
|
14120
14069
|
} catch (err) {
|
|
14121
14070
|
logger.warn(`cohort-sync: after_tool_call activity failed: ${String(err)}`);
|
|
14122
14071
|
}
|
|
@@ -14125,7 +14074,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14125
14074
|
diag("HOOK_message_received_RAW", {
|
|
14126
14075
|
ctx: dumpCtx(ctx),
|
|
14127
14076
|
event: dumpEvent(_event),
|
|
14128
|
-
bridgeStateBefore:
|
|
14077
|
+
bridgeStateBefore: Object.fromEntries(getChannelAgentBridge())
|
|
14129
14078
|
});
|
|
14130
14079
|
const agentName = resolveAgentFromContext(ctx);
|
|
14131
14080
|
const channel = ctx.channelId;
|
|
@@ -14140,7 +14089,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14140
14089
|
const entry = buildActivityEntry(agentName, "message_received", {
|
|
14141
14090
|
channel: channel ?? "unknown"
|
|
14142
14091
|
});
|
|
14143
|
-
if (entry)
|
|
14092
|
+
if (entry) activityBatch.add(entry);
|
|
14144
14093
|
} catch (err) {
|
|
14145
14094
|
logger.warn(`cohort-sync: message_received activity failed: ${String(err)}`);
|
|
14146
14095
|
}
|
|
@@ -14149,7 +14098,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14149
14098
|
diag("HOOK_message_sent_RAW", {
|
|
14150
14099
|
ctx: dumpCtx(ctx),
|
|
14151
14100
|
event: dumpEvent(event),
|
|
14152
|
-
bridgeStateBefore:
|
|
14101
|
+
bridgeStateBefore: Object.fromEntries(getChannelAgentBridge())
|
|
14153
14102
|
});
|
|
14154
14103
|
const agentName = resolveAgentFromContext(ctx);
|
|
14155
14104
|
const channel = ctx.channelId;
|
|
@@ -14168,7 +14117,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14168
14117
|
success: event.success,
|
|
14169
14118
|
error: event.error
|
|
14170
14119
|
});
|
|
14171
|
-
if (entry)
|
|
14120
|
+
if (entry) activityBatch.add(entry);
|
|
14172
14121
|
} catch (err) {
|
|
14173
14122
|
logger.warn(`cohort-sync: message_sent activity failed: ${String(err)}`);
|
|
14174
14123
|
}
|
|
@@ -14181,7 +14130,7 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14181
14130
|
const entry = buildActivityEntry(agentName, "before_compaction", {
|
|
14182
14131
|
sessionKey: ctx.sessionKey
|
|
14183
14132
|
});
|
|
14184
|
-
if (entry)
|
|
14133
|
+
if (entry) activityBatch.add(entry);
|
|
14185
14134
|
} catch (err) {
|
|
14186
14135
|
logger.warn(`cohort-sync: before_compaction activity failed: ${String(err)}`);
|
|
14187
14136
|
}
|
|
@@ -14195,25 +14144,24 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14195
14144
|
reason: event.reason,
|
|
14196
14145
|
sessionKey: ctx.sessionKey
|
|
14197
14146
|
});
|
|
14198
|
-
if (entry)
|
|
14147
|
+
if (entry) activityBatch.add(entry);
|
|
14199
14148
|
} catch (err) {
|
|
14200
14149
|
logger.warn(`cohort-sync: before_reset activity failed: ${String(err)}`);
|
|
14201
14150
|
}
|
|
14202
14151
|
});
|
|
14203
14152
|
api.on("gateway_stop", async () => {
|
|
14204
|
-
diag("HOOK_gateway_stop", { bridgeState:
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
});
|
|
14153
|
+
diag("HOOK_gateway_stop", { bridgeState: Object.fromEntries(getChannelAgentBridge()) });
|
|
14154
|
+
if (keepaliveInterval) {
|
|
14155
|
+
clearInterval(keepaliveInterval);
|
|
14156
|
+
keepaliveInterval = null;
|
|
14157
|
+
}
|
|
14158
|
+
activityBatch.drain();
|
|
14211
14159
|
const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
|
|
14212
14160
|
for (const agentId of allAgentIds) {
|
|
14213
14161
|
const agentName = resolveAgentName(agentId);
|
|
14214
14162
|
try {
|
|
14215
|
-
|
|
14216
|
-
const snapshot =
|
|
14163
|
+
tracker2.updateStatus(agentName, "unreachable");
|
|
14164
|
+
const snapshot = tracker2.getTelemetrySnapshot(agentName);
|
|
14217
14165
|
if (snapshot) {
|
|
14218
14166
|
await pushTelemetry(cfg.apiKey, { ...snapshot, pluginVersion: PLUGIN_VERSION });
|
|
14219
14167
|
}
|
|
@@ -14221,15 +14169,20 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
14221
14169
|
logger.warn(`cohort-sync: final unreachable push failed for ${agentName}: ${String(err)}`);
|
|
14222
14170
|
}
|
|
14223
14171
|
}
|
|
14224
|
-
saveSessionsToDisk(
|
|
14172
|
+
saveSessionsToDisk(tracker2);
|
|
14173
|
+
if (persistentGwClient) {
|
|
14174
|
+
persistentGwClient.close();
|
|
14175
|
+
persistentGwClient = null;
|
|
14176
|
+
gwClientInitialized = false;
|
|
14177
|
+
}
|
|
14225
14178
|
try {
|
|
14226
14179
|
await markAllUnreachable(cfg, logger);
|
|
14227
14180
|
} catch (err) {
|
|
14228
14181
|
logger.warn(`cohort-sync: markAllUnreachable failed: ${String(err)}`);
|
|
14229
14182
|
}
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
logger.info("cohort-sync:
|
|
14183
|
+
tracker2.clear();
|
|
14184
|
+
closeBridge();
|
|
14185
|
+
logger.info("cohort-sync: gateway stopped, all resources cleaned up");
|
|
14233
14186
|
});
|
|
14234
14187
|
}
|
|
14235
14188
|
|
|
@@ -14394,14 +14347,19 @@ function registerCohortCli(ctx, cfg) {
|
|
|
14394
14347
|
|
|
14395
14348
|
// index.ts
|
|
14396
14349
|
init_keychain();
|
|
14397
|
-
var DEFAULT_API_URL = "https://fortunate-chipmunk-286.convex.site";
|
|
14398
14350
|
var plugin = {
|
|
14399
14351
|
id: "cohort-sync",
|
|
14400
14352
|
name: "Cohort Sync",
|
|
14401
14353
|
description: "Syncs agent status and skills to Cohort dashboard",
|
|
14402
14354
|
register(api) {
|
|
14403
14355
|
const cfg = api.pluginConfig;
|
|
14404
|
-
const apiUrl = cfg?.apiUrl
|
|
14356
|
+
const apiUrl = cfg?.apiUrl;
|
|
14357
|
+
if (!apiUrl) {
|
|
14358
|
+
api.logger.error(
|
|
14359
|
+
"cohort-sync: apiUrl is required in plugin config \u2014 set it in your OpenClaw configuration"
|
|
14360
|
+
);
|
|
14361
|
+
return;
|
|
14362
|
+
}
|
|
14405
14363
|
api.registerCli(
|
|
14406
14364
|
(ctx) => registerCohortCli(ctx, {
|
|
14407
14365
|
apiUrl,
|
|
@@ -14435,12 +14393,11 @@ var plugin = {
|
|
|
14435
14393
|
apiUrl,
|
|
14436
14394
|
apiKey,
|
|
14437
14395
|
stateDir: svcCtx.stateDir,
|
|
14438
|
-
agentNameMap: cfg?.agentNameMap
|
|
14439
|
-
syncIntervalMs: cfg?.syncIntervalMs
|
|
14396
|
+
agentNameMap: cfg?.agentNameMap
|
|
14440
14397
|
});
|
|
14441
14398
|
},
|
|
14442
14399
|
async stop() {
|
|
14443
|
-
|
|
14400
|
+
closeBridge();
|
|
14444
14401
|
api.logger.info("cohort-sync: service stopped");
|
|
14445
14402
|
}
|
|
14446
14403
|
});
|