@cfio/cohort-sync 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +754 -312
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -88,8 +88,8 @@ var init_keychain = __esm({
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
// src/hooks.ts
|
|
91
|
-
import
|
|
92
|
-
import
|
|
91
|
+
import fs3 from "node:fs";
|
|
92
|
+
import path3 from "node:path";
|
|
93
93
|
|
|
94
94
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
95
95
|
var value_exports = {};
|
|
@@ -4554,12 +4554,12 @@ function createApi(pathParts = []) {
|
|
|
4554
4554
|
`API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
|
|
4555
4555
|
);
|
|
4556
4556
|
}
|
|
4557
|
-
const
|
|
4557
|
+
const path4 = pathParts.slice(0, -1).join("/");
|
|
4558
4558
|
const exportName = pathParts[pathParts.length - 1];
|
|
4559
4559
|
if (exportName === "default") {
|
|
4560
|
-
return
|
|
4560
|
+
return path4;
|
|
4561
4561
|
} else {
|
|
4562
|
-
return
|
|
4562
|
+
return path4 + ":" + exportName;
|
|
4563
4563
|
}
|
|
4564
4564
|
} else if (prop === Symbol.toStringTag) {
|
|
4565
4565
|
return "FunctionReference";
|
|
@@ -7628,16 +7628,16 @@ var require_constants = __commonJS({
|
|
|
7628
7628
|
});
|
|
7629
7629
|
var require_node_gyp_build = __commonJS({
|
|
7630
7630
|
"../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
7631
|
-
var
|
|
7632
|
-
var
|
|
7633
|
-
var
|
|
7631
|
+
var fs4 = __require("fs");
|
|
7632
|
+
var path4 = __require("path");
|
|
7633
|
+
var os3 = __require("os");
|
|
7634
7634
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
7635
7635
|
var vars = process.config && process.config.variables || {};
|
|
7636
7636
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
7637
7637
|
var abi = process.versions.modules;
|
|
7638
7638
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
7639
|
-
var arch = process.env.npm_config_arch ||
|
|
7640
|
-
var platform = process.env.npm_config_platform ||
|
|
7639
|
+
var arch = process.env.npm_config_arch || os3.arch();
|
|
7640
|
+
var platform = process.env.npm_config_platform || os3.platform();
|
|
7641
7641
|
var libc = process.env.LIBC || (isAlpine(platform) ? "musl" : "glibc");
|
|
7642
7642
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
7643
7643
|
var uv = (process.versions.uv || "").split(".")[0];
|
|
@@ -7646,21 +7646,21 @@ var require_node_gyp_build = __commonJS({
|
|
|
7646
7646
|
return runtimeRequire(load.resolve(dir));
|
|
7647
7647
|
}
|
|
7648
7648
|
load.resolve = load.path = function(dir) {
|
|
7649
|
-
dir =
|
|
7649
|
+
dir = path4.resolve(dir || ".");
|
|
7650
7650
|
try {
|
|
7651
|
-
var name = runtimeRequire(
|
|
7651
|
+
var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
|
|
7652
7652
|
if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
|
|
7653
7653
|
} catch (err) {
|
|
7654
7654
|
}
|
|
7655
7655
|
if (!prebuildsOnly) {
|
|
7656
|
-
var release = getFirst(
|
|
7656
|
+
var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
|
|
7657
7657
|
if (release) return release;
|
|
7658
|
-
var debug = getFirst(
|
|
7658
|
+
var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
|
|
7659
7659
|
if (debug) return debug;
|
|
7660
7660
|
}
|
|
7661
7661
|
var prebuild = resolve(dir);
|
|
7662
7662
|
if (prebuild) return prebuild;
|
|
7663
|
-
var nearby = resolve(
|
|
7663
|
+
var nearby = resolve(path4.dirname(process.execPath));
|
|
7664
7664
|
if (nearby) return nearby;
|
|
7665
7665
|
var target = [
|
|
7666
7666
|
"platform=" + platform,
|
|
@@ -7677,26 +7677,26 @@ var require_node_gyp_build = __commonJS({
|
|
|
7677
7677
|
].filter(Boolean).join(" ");
|
|
7678
7678
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
7679
7679
|
function resolve(dir2) {
|
|
7680
|
-
var tuples = readdirSync(
|
|
7680
|
+
var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
|
|
7681
7681
|
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
|
|
7682
7682
|
if (!tuple) return;
|
|
7683
|
-
var prebuilds =
|
|
7683
|
+
var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
|
|
7684
7684
|
var parsed = readdirSync(prebuilds).map(parseTags);
|
|
7685
7685
|
var candidates = parsed.filter(matchTags(runtime, abi));
|
|
7686
7686
|
var winner = candidates.sort(compareTags(runtime))[0];
|
|
7687
|
-
if (winner) return
|
|
7687
|
+
if (winner) return path4.join(prebuilds, winner.file);
|
|
7688
7688
|
}
|
|
7689
7689
|
};
|
|
7690
7690
|
function readdirSync(dir) {
|
|
7691
7691
|
try {
|
|
7692
|
-
return
|
|
7692
|
+
return fs4.readdirSync(dir);
|
|
7693
7693
|
} catch (err) {
|
|
7694
7694
|
return [];
|
|
7695
7695
|
}
|
|
7696
7696
|
}
|
|
7697
7697
|
function getFirst(dir, filter) {
|
|
7698
7698
|
var files = readdirSync(dir).filter(filter);
|
|
7699
|
-
return files[0] &&
|
|
7699
|
+
return files[0] && path4.join(dir, files[0]);
|
|
7700
7700
|
}
|
|
7701
7701
|
function matchBuild(name) {
|
|
7702
7702
|
return /\.node$/.test(name);
|
|
@@ -7783,7 +7783,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
7783
7783
|
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
|
|
7784
7784
|
}
|
|
7785
7785
|
function isAlpine(platform2) {
|
|
7786
|
-
return platform2 === "linux" &&
|
|
7786
|
+
return platform2 === "linux" && fs4.existsSync("/etc/alpine-release");
|
|
7787
7787
|
}
|
|
7788
7788
|
load.parseTags = parseTags;
|
|
7789
7789
|
load.matchTags = matchTags;
|
|
@@ -11502,110 +11502,6 @@ var _systemSchema = defineSchema({
|
|
|
11502
11502
|
})
|
|
11503
11503
|
});
|
|
11504
11504
|
|
|
11505
|
-
// src/gateway-rpc.ts
|
|
11506
|
-
import crypto from "node:crypto";
|
|
11507
|
-
function buildRequestFrame(id, method, params) {
|
|
11508
|
-
return { id, type: "req", method, params };
|
|
11509
|
-
}
|
|
11510
|
-
function getPendingRequests() {
|
|
11511
|
-
const g = globalThis;
|
|
11512
|
-
const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
|
|
11513
|
-
if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
|
|
11514
|
-
return hot.pendingGatewayRequests;
|
|
11515
|
-
}
|
|
11516
|
-
var authReady = null;
|
|
11517
|
-
function openGatewayConnection(port, token, logger) {
|
|
11518
|
-
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
11519
|
-
let resolveAuth;
|
|
11520
|
-
let rejectAuth;
|
|
11521
|
-
let authSettled = false;
|
|
11522
|
-
authReady = new Promise((res, rej) => {
|
|
11523
|
-
resolveAuth = res;
|
|
11524
|
-
rejectAuth = rej;
|
|
11525
|
-
});
|
|
11526
|
-
ws.addEventListener("open", () => {
|
|
11527
|
-
ws.send(JSON.stringify({
|
|
11528
|
-
minProtocol: 1,
|
|
11529
|
-
maxProtocol: 1,
|
|
11530
|
-
client: {
|
|
11531
|
-
id: "gateway-client",
|
|
11532
|
-
version: "1.0.0",
|
|
11533
|
-
platform: process.platform,
|
|
11534
|
-
mode: "backend"
|
|
11535
|
-
},
|
|
11536
|
-
auth: { token }
|
|
11537
|
-
}));
|
|
11538
|
-
logger.info(`cohort-sync: gateway WS connected to port ${port}, awaiting hello-ok`);
|
|
11539
|
-
});
|
|
11540
|
-
ws.addEventListener("message", (event) => {
|
|
11541
|
-
try {
|
|
11542
|
-
const data = JSON.parse(String(event.data));
|
|
11543
|
-
if (data.type === "hello-ok") {
|
|
11544
|
-
authSettled = true;
|
|
11545
|
-
resolveAuth();
|
|
11546
|
-
logger.info("cohort-sync: gateway WS authenticated");
|
|
11547
|
-
return;
|
|
11548
|
-
}
|
|
11549
|
-
if (data.type === "hello-error" || data.type === "error") {
|
|
11550
|
-
authSettled = true;
|
|
11551
|
-
rejectAuth(new Error(`Gateway auth failed: ${data.message ?? data.error ?? "unknown"}`));
|
|
11552
|
-
ws.close();
|
|
11553
|
-
return;
|
|
11554
|
-
}
|
|
11555
|
-
const pending = getPendingRequests();
|
|
11556
|
-
if (data.type === "res" && data.id && pending.has(data.id)) {
|
|
11557
|
-
const entry = pending.get(data.id);
|
|
11558
|
-
pending.delete(data.id);
|
|
11559
|
-
clearTimeout(entry.timer);
|
|
11560
|
-
if (data.ok) {
|
|
11561
|
-
entry.resolve(data.payload);
|
|
11562
|
-
} else {
|
|
11563
|
-
entry.reject(new Error(`Gateway method failed: ${data.error?.message ?? "unknown"}`));
|
|
11564
|
-
}
|
|
11565
|
-
}
|
|
11566
|
-
} catch {
|
|
11567
|
-
}
|
|
11568
|
-
});
|
|
11569
|
-
ws.addEventListener("close", () => {
|
|
11570
|
-
logger.warn("cohort-sync: gateway WS closed");
|
|
11571
|
-
if (!authSettled) {
|
|
11572
|
-
authSettled = true;
|
|
11573
|
-
rejectAuth(new Error("Gateway WS closed during auth"));
|
|
11574
|
-
}
|
|
11575
|
-
const pending = getPendingRequests();
|
|
11576
|
-
for (const [, { reject, timer }] of pending) {
|
|
11577
|
-
clearTimeout(timer);
|
|
11578
|
-
reject(new Error("Gateway WS closed"));
|
|
11579
|
-
}
|
|
11580
|
-
pending.clear();
|
|
11581
|
-
});
|
|
11582
|
-
ws.addEventListener("error", (err) => {
|
|
11583
|
-
logger.error(`cohort-sync: gateway WS error: ${String(err)}`);
|
|
11584
|
-
});
|
|
11585
|
-
return ws;
|
|
11586
|
-
}
|
|
11587
|
-
async function callGatewayMethod(ws, method, params, timeoutMs = 1e4) {
|
|
11588
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
11589
|
-
throw new Error("Gateway WS not connected");
|
|
11590
|
-
}
|
|
11591
|
-
if (authReady) await authReady;
|
|
11592
|
-
const id = crypto.randomUUID();
|
|
11593
|
-
const frame = buildRequestFrame(id, method, params);
|
|
11594
|
-
const pending = getPendingRequests();
|
|
11595
|
-
return new Promise((resolve, reject) => {
|
|
11596
|
-
const timer = setTimeout(() => {
|
|
11597
|
-
pending.delete(id);
|
|
11598
|
-
reject(new Error(`Gateway method ${method} timed out after ${timeoutMs}ms`));
|
|
11599
|
-
}, timeoutMs);
|
|
11600
|
-
pending.set(id, {
|
|
11601
|
-
resolve,
|
|
11602
|
-
reject,
|
|
11603
|
-
timer
|
|
11604
|
-
});
|
|
11605
|
-
ws.send(JSON.stringify(frame));
|
|
11606
|
-
});
|
|
11607
|
-
}
|
|
11608
|
-
|
|
11609
11505
|
// src/cron-mapping.ts
|
|
11610
11506
|
function formatSchedule(s) {
|
|
11611
11507
|
switch (s.kind) {
|
|
@@ -11745,9 +11641,9 @@ function getHotState() {
|
|
|
11745
11641
|
intervals: { heartbeat: null, activityFlush: null },
|
|
11746
11642
|
activityBuffer: [],
|
|
11747
11643
|
channelAgentBridge: {},
|
|
11748
|
-
gatewayWs: null,
|
|
11749
11644
|
gatewayPort: null,
|
|
11750
11645
|
gatewayToken: null,
|
|
11646
|
+
gatewayProtocolClient: null,
|
|
11751
11647
|
commandSubscription: null
|
|
11752
11648
|
};
|
|
11753
11649
|
globalThis[HOT_KEY] = state;
|
|
@@ -11904,92 +11800,111 @@ function initCommandSubscription(cfg, logger, resolveAgentName) {
|
|
|
11904
11800
|
return;
|
|
11905
11801
|
}
|
|
11906
11802
|
if (cmd.type.startsWith("cron")) {
|
|
11907
|
-
const
|
|
11908
|
-
|
|
11909
|
-
|
|
11803
|
+
const hotState = getHotState();
|
|
11804
|
+
const port = hotState.gatewayPort;
|
|
11805
|
+
const token = hotState.gatewayToken;
|
|
11806
|
+
if (!port || !token) {
|
|
11807
|
+
logger.warn(`cohort-sync: no gateway port/token, cannot execute ${cmd.type}`);
|
|
11910
11808
|
continue;
|
|
11911
11809
|
}
|
|
11912
|
-
const
|
|
11913
|
-
|
|
11914
|
-
await
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11810
|
+
const gwClient = new GatewayClient(port, token, logger);
|
|
11811
|
+
try {
|
|
11812
|
+
await gwClient.connect();
|
|
11813
|
+
const nameMap = cfg.agentNameMap ?? {};
|
|
11814
|
+
switch (cmd.type) {
|
|
11815
|
+
case "cronEnable":
|
|
11816
|
+
await gwClient.request("cron.update", {
|
|
11817
|
+
jobId: cmd.payload?.jobId,
|
|
11818
|
+
patch: { enabled: true }
|
|
11819
|
+
});
|
|
11820
|
+
break;
|
|
11821
|
+
case "cronDisable":
|
|
11822
|
+
await gwClient.request("cron.update", {
|
|
11823
|
+
jobId: cmd.payload?.jobId,
|
|
11824
|
+
patch: { enabled: false }
|
|
11825
|
+
});
|
|
11826
|
+
break;
|
|
11827
|
+
case "cronDelete":
|
|
11828
|
+
await gwClient.request("cron.remove", {
|
|
11829
|
+
jobId: cmd.payload?.jobId
|
|
11830
|
+
});
|
|
11831
|
+
break;
|
|
11832
|
+
case "cronRunNow": {
|
|
11833
|
+
const runResult = await gwClient.request(
|
|
11834
|
+
"cron.run",
|
|
11835
|
+
{ jobId: cmd.payload?.jobId }
|
|
11836
|
+
);
|
|
11837
|
+
if (runResult?.ok && runResult?.ran) {
|
|
11838
|
+
const jobId = cmd.payload?.jobId;
|
|
11839
|
+
let polls = 0;
|
|
11840
|
+
const pollInterval = setInterval(async () => {
|
|
11841
|
+
polls++;
|
|
11842
|
+
if (polls >= 15) {
|
|
11843
|
+
clearInterval(pollInterval);
|
|
11844
|
+
return;
|
|
11845
|
+
}
|
|
11846
|
+
try {
|
|
11847
|
+
const pollClient = getHotState().gatewayProtocolClient;
|
|
11848
|
+
if (!pollClient || !pollClient.isAlive()) {
|
|
11849
|
+
clearInterval(pollInterval);
|
|
11850
|
+
return;
|
|
11851
|
+
}
|
|
11852
|
+
const pollResult = await pollClient.request("cron.list");
|
|
11853
|
+
const freshJobs = Array.isArray(pollResult) ? pollResult : pollResult?.jobs ?? [];
|
|
11854
|
+
const job = freshJobs.find((j) => j.id === jobId);
|
|
11855
|
+
if (job && !job.state?.runningAtMs) {
|
|
11856
|
+
clearInterval(pollInterval);
|
|
11857
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11858
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11859
|
+
}
|
|
11860
|
+
} catch {
|
|
11861
|
+
}
|
|
11862
|
+
}, 2e3);
|
|
11941
11863
|
}
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11864
|
+
break;
|
|
11865
|
+
}
|
|
11866
|
+
case "cronCreate": {
|
|
11867
|
+
const agentId = reverseResolveAgentName(cmd.payload?.agentId ?? "main", nameMap);
|
|
11868
|
+
await gwClient.request("cron.add", {
|
|
11869
|
+
job: {
|
|
11870
|
+
agentId,
|
|
11871
|
+
name: cmd.payload?.name,
|
|
11872
|
+
enabled: true,
|
|
11873
|
+
schedule: cmd.payload?.schedule,
|
|
11874
|
+
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11875
|
+
sessionTarget: "isolated",
|
|
11876
|
+
wakeMode: "now"
|
|
11954
11877
|
}
|
|
11955
|
-
}
|
|
11956
|
-
|
|
11957
|
-
}
|
|
11878
|
+
});
|
|
11879
|
+
break;
|
|
11880
|
+
}
|
|
11881
|
+
case "cronUpdate": {
|
|
11882
|
+
const patch = {};
|
|
11883
|
+
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11884
|
+
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11885
|
+
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11886
|
+
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11887
|
+
await gwClient.request("cron.update", {
|
|
11888
|
+
jobId: cmd.payload?.jobId,
|
|
11889
|
+
patch
|
|
11890
|
+
});
|
|
11891
|
+
break;
|
|
11892
|
+
}
|
|
11893
|
+
default:
|
|
11894
|
+
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11958
11895
|
}
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
payload: { kind: "agentTurn", message: cmd.payload?.message },
|
|
11968
|
-
sessionTarget: "isolated",
|
|
11969
|
-
wakeMode: "now"
|
|
11896
|
+
if (gwClient.isAlive()) {
|
|
11897
|
+
try {
|
|
11898
|
+
const snapResult = await gwClient.request("cron.list");
|
|
11899
|
+
const freshJobs = Array.isArray(snapResult) ? snapResult : snapResult?.jobs ?? [];
|
|
11900
|
+
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11901
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11902
|
+
} catch (snapErr) {
|
|
11903
|
+
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11970
11904
|
}
|
|
11971
|
-
});
|
|
11972
|
-
} else if (cmd.type === "cronUpdate") {
|
|
11973
|
-
const patch = {};
|
|
11974
|
-
if (cmd.payload?.name) patch.name = cmd.payload.name;
|
|
11975
|
-
if (cmd.payload?.schedule) patch.schedule = cmd.payload.schedule;
|
|
11976
|
-
if (cmd.payload?.message) patch.payload = { kind: "agentTurn", message: cmd.payload.message };
|
|
11977
|
-
if (cmd.payload?.agentId) patch.agentId = reverseResolveAgentName(cmd.payload.agentId, nameMap);
|
|
11978
|
-
await callGatewayMethod(ws, "cohort-sync/cron-update", {
|
|
11979
|
-
jobId: cmd.payload?.jobId,
|
|
11980
|
-
patch
|
|
11981
|
-
});
|
|
11982
|
-
} else {
|
|
11983
|
-
logger.warn(`cohort-sync: unknown cron command type: ${cmd.type}`);
|
|
11984
|
-
}
|
|
11985
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
11986
|
-
try {
|
|
11987
|
-
const freshJobs = await callGatewayMethod(ws, "cohort-sync/cron-list");
|
|
11988
|
-
const mapped = freshJobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
11989
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
11990
|
-
} catch (snapErr) {
|
|
11991
|
-
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11992
11905
|
}
|
|
11906
|
+
} finally {
|
|
11907
|
+
gwClient.close();
|
|
11993
11908
|
}
|
|
11994
11909
|
} else {
|
|
11995
11910
|
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
@@ -12044,6 +11959,13 @@ function closeSubscription() {
|
|
|
12044
11959
|
}
|
|
12045
11960
|
state.commandSubscription = null;
|
|
12046
11961
|
}
|
|
11962
|
+
if (state.gatewayProtocolClient) {
|
|
11963
|
+
try {
|
|
11964
|
+
state.gatewayProtocolClient.close();
|
|
11965
|
+
} catch {
|
|
11966
|
+
}
|
|
11967
|
+
state.gatewayProtocolClient = null;
|
|
11968
|
+
}
|
|
12047
11969
|
client?.close();
|
|
12048
11970
|
client = null;
|
|
12049
11971
|
clearHotState();
|
|
@@ -12152,31 +12074,31 @@ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
|
12152
12074
|
function normalizeStatus(status) {
|
|
12153
12075
|
return VALID_STATUSES.has(status) ? status : "idle";
|
|
12154
12076
|
}
|
|
12155
|
-
async function v1Get(apiUrl, apiKey,
|
|
12156
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${
|
|
12077
|
+
async function v1Get(apiUrl, apiKey, path4) {
|
|
12078
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12157
12079
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
12158
12080
|
signal: AbortSignal.timeout(1e4)
|
|
12159
12081
|
});
|
|
12160
|
-
if (!res.ok) throw new Error(`GET ${
|
|
12082
|
+
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
12161
12083
|
return res.json();
|
|
12162
12084
|
}
|
|
12163
|
-
async function v1Patch(apiUrl, apiKey,
|
|
12164
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${
|
|
12085
|
+
async function v1Patch(apiUrl, apiKey, path4, body) {
|
|
12086
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12165
12087
|
method: "PATCH",
|
|
12166
12088
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12167
12089
|
body: JSON.stringify(body),
|
|
12168
12090
|
signal: AbortSignal.timeout(1e4)
|
|
12169
12091
|
});
|
|
12170
|
-
if (!res.ok) throw new Error(`PATCH ${
|
|
12092
|
+
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
12171
12093
|
}
|
|
12172
|
-
async function v1Post(apiUrl, apiKey,
|
|
12173
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${
|
|
12094
|
+
async function v1Post(apiUrl, apiKey, path4, body) {
|
|
12095
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}${path4}`, {
|
|
12174
12096
|
method: "POST",
|
|
12175
12097
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
12176
12098
|
body: JSON.stringify(body),
|
|
12177
12099
|
signal: AbortSignal.timeout(1e4)
|
|
12178
12100
|
});
|
|
12179
|
-
if (!res.ok) throw new Error(`POST ${
|
|
12101
|
+
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
12180
12102
|
}
|
|
12181
12103
|
async function checkForUpdate(currentVersion, logger) {
|
|
12182
12104
|
try {
|
|
@@ -12344,64 +12266,542 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
|
|
|
12344
12266
|
logger.info("cohort-sync: full sync complete");
|
|
12345
12267
|
}
|
|
12346
12268
|
|
|
12347
|
-
// src/gateway-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12269
|
+
// src/gateway-client.ts
|
|
12270
|
+
import crypto2 from "node:crypto";
|
|
12271
|
+
|
|
12272
|
+
// src/diag.ts
|
|
12273
|
+
import fs from "node:fs";
|
|
12274
|
+
import path from "node:path";
|
|
12275
|
+
import os from "node:os";
|
|
12276
|
+
var LOG_DIR = path.join(os.homedir(), ".openclaw", "logs", "cohort-sync");
|
|
12277
|
+
var LOG_PATH = path.join(LOG_DIR, "diag.log");
|
|
12278
|
+
var LOG_PATH_ROTATED = path.join(LOG_DIR, "diag.log.1");
|
|
12279
|
+
var MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
12280
|
+
var isDebug = process.env.COHORT_SYNC_DEBUG === "1";
|
|
12281
|
+
try {
|
|
12282
|
+
fs.mkdirSync(LOG_DIR, { recursive: true, mode: 448 });
|
|
12283
|
+
} catch {
|
|
12284
|
+
}
|
|
12285
|
+
var writesSinceCheck = 0;
|
|
12286
|
+
function diag(label, data) {
|
|
12287
|
+
if (!isDebug && !label.startsWith("HOOK_") && !label.startsWith("MODULE_") && !label.startsWith("REGISTER_") && !label.startsWith("GW_WS_AUTH") && !label.startsWith("GW_WS_CLOSED") && !label.startsWith("GW_WS_ERROR") && !label.startsWith("GW_CLIENT_") && !label.startsWith("HEARTBEAT_CRON")) {
|
|
12288
|
+
return;
|
|
12289
|
+
}
|
|
12290
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
12291
|
+
const sanitized = data ? " " + JSON.stringify(data, (_k, v2) => {
|
|
12292
|
+
if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
|
|
12293
|
+
return v2;
|
|
12294
|
+
}) : "";
|
|
12295
|
+
const line = `[${ts}] ${label}${sanitized}
|
|
12296
|
+
`;
|
|
12297
|
+
try {
|
|
12298
|
+
fs.appendFileSync(LOG_PATH, line, { mode: 384 });
|
|
12299
|
+
if (++writesSinceCheck >= 100) {
|
|
12300
|
+
writesSinceCheck = 0;
|
|
12301
|
+
const stat = fs.statSync(LOG_PATH);
|
|
12302
|
+
if (stat.size > MAX_LOG_SIZE) {
|
|
12303
|
+
try {
|
|
12304
|
+
fs.unlinkSync(LOG_PATH_ROTATED);
|
|
12305
|
+
} catch {
|
|
12306
|
+
}
|
|
12307
|
+
fs.renameSync(LOG_PATH, LOG_PATH_ROTATED);
|
|
12308
|
+
}
|
|
12358
12309
|
}
|
|
12359
|
-
}
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12310
|
+
} catch {
|
|
12311
|
+
}
|
|
12312
|
+
}
|
|
12313
|
+
|
|
12314
|
+
// src/device-identity-crypto.ts
|
|
12315
|
+
import crypto from "node:crypto";
|
|
12316
|
+
import fs2 from "node:fs";
|
|
12317
|
+
import path2 from "node:path";
|
|
12318
|
+
import os2 from "node:os";
|
|
12319
|
+
var IDENTITY_PATH = path2.join(os2.homedir(), ".openclaw", "extensions", "cohort-sync", ".device-identity.json");
|
|
12320
|
+
function loadOrCreateDeviceIdentity() {
|
|
12321
|
+
try {
|
|
12322
|
+
const data = JSON.parse(fs2.readFileSync(IDENTITY_PATH, "utf-8"));
|
|
12323
|
+
if (data.deviceId && data.publicKeyPem && data.privateKeyPem) {
|
|
12324
|
+
diag("GW_CLIENT_DEVICE_IDENTITY_LOADED", { deviceId: data.deviceId });
|
|
12325
|
+
return data;
|
|
12369
12326
|
}
|
|
12327
|
+
} catch {
|
|
12328
|
+
}
|
|
12329
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
12330
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
12331
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
12332
|
+
const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
|
|
12333
|
+
const rawPublicKey = publicKeyDer.subarray(publicKeyDer.length - 32);
|
|
12334
|
+
const deviceId = crypto.createHash("sha256").update(rawPublicKey).digest("hex");
|
|
12335
|
+
const identity = { deviceId, publicKeyPem, privateKeyPem };
|
|
12336
|
+
try {
|
|
12337
|
+
fs2.writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
12338
|
+
diag("GW_CLIENT_DEVICE_IDENTITY_CREATED", { deviceId });
|
|
12339
|
+
} catch (err) {
|
|
12340
|
+
diag("GW_CLIENT_DEVICE_IDENTITY_WRITE_FAILED", { error: String(err) });
|
|
12341
|
+
}
|
|
12342
|
+
return identity;
|
|
12343
|
+
}
|
|
12344
|
+
function normalizeMetadata(value) {
|
|
12345
|
+
if (typeof value !== "string") return "";
|
|
12346
|
+
const trimmed = value.trim();
|
|
12347
|
+
if (!trimmed) return "";
|
|
12348
|
+
return trimmed.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
|
|
12349
|
+
}
|
|
12350
|
+
function buildDeviceAuthPayloadV3(params) {
|
|
12351
|
+
return [
|
|
12352
|
+
"v3",
|
|
12353
|
+
params.deviceId,
|
|
12354
|
+
params.clientId,
|
|
12355
|
+
params.clientMode,
|
|
12356
|
+
params.role,
|
|
12357
|
+
params.scopes.join(","),
|
|
12358
|
+
String(params.signedAtMs),
|
|
12359
|
+
params.token ?? "",
|
|
12360
|
+
params.nonce,
|
|
12361
|
+
normalizeMetadata(params.platform),
|
|
12362
|
+
normalizeMetadata(params.deviceFamily)
|
|
12363
|
+
].join("|");
|
|
12364
|
+
}
|
|
12365
|
+
function signPayload(privateKeyPem, payload) {
|
|
12366
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
12367
|
+
const signature = crypto.sign(null, Buffer.from(payload, "utf-8"), key);
|
|
12368
|
+
return signature.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
12369
|
+
}
|
|
12370
|
+
|
|
12371
|
+
// src/gateway-client.ts
|
|
12372
|
+
var ALLOWED_METHODS = /* @__PURE__ */ new Set([
|
|
12373
|
+
"cron.list",
|
|
12374
|
+
"cron.add",
|
|
12375
|
+
"cron.update",
|
|
12376
|
+
"cron.remove",
|
|
12377
|
+
"cron.run",
|
|
12378
|
+
"cron.runs",
|
|
12379
|
+
"cron.status",
|
|
12380
|
+
"channels.status",
|
|
12381
|
+
"sessions.list",
|
|
12382
|
+
"sessions.preview",
|
|
12383
|
+
"agent",
|
|
12384
|
+
"snapshot",
|
|
12385
|
+
"system.presence"
|
|
12386
|
+
]);
|
|
12387
|
+
function buildConnectFrame(id, token, pluginVersion, identity, nonce) {
|
|
12388
|
+
const signedAtMs = Date.now();
|
|
12389
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
12390
|
+
deviceId: identity.deviceId,
|
|
12391
|
+
clientId: "gateway-client",
|
|
12392
|
+
clientMode: "backend",
|
|
12393
|
+
role: "operator",
|
|
12394
|
+
scopes: ["operator.read", "operator.write"],
|
|
12395
|
+
signedAtMs,
|
|
12396
|
+
token,
|
|
12397
|
+
nonce,
|
|
12398
|
+
platform: process.platform,
|
|
12399
|
+
deviceFamily: null
|
|
12370
12400
|
});
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12401
|
+
const signature = signPayload(identity.privateKeyPem, payload);
|
|
12402
|
+
return {
|
|
12403
|
+
type: "req",
|
|
12404
|
+
id,
|
|
12405
|
+
method: "connect",
|
|
12406
|
+
params: {
|
|
12407
|
+
minProtocol: 3,
|
|
12408
|
+
maxProtocol: 3,
|
|
12409
|
+
client: {
|
|
12410
|
+
id: "gateway-client",
|
|
12411
|
+
version: pluginVersion,
|
|
12412
|
+
platform: process.platform,
|
|
12413
|
+
mode: "backend"
|
|
12414
|
+
},
|
|
12415
|
+
role: "operator",
|
|
12416
|
+
scopes: ["operator.read", "operator.write"],
|
|
12417
|
+
auth: { token },
|
|
12418
|
+
device: {
|
|
12419
|
+
id: identity.deviceId,
|
|
12420
|
+
publicKey: identity.publicKeyPem,
|
|
12421
|
+
signature,
|
|
12422
|
+
signedAt: signedAtMs,
|
|
12423
|
+
nonce
|
|
12424
|
+
},
|
|
12425
|
+
locale: "en-US",
|
|
12426
|
+
userAgent: `cohort-sync/${pluginVersion}`
|
|
12427
|
+
}
|
|
12428
|
+
};
|
|
12429
|
+
}
|
|
12430
|
+
function parseHelloOk(response) {
|
|
12431
|
+
if (!response.ok) {
|
|
12432
|
+
const msg = response.error?.message ?? "unknown error";
|
|
12433
|
+
throw new Error(`Gateway connect error: ${msg}`);
|
|
12434
|
+
}
|
|
12435
|
+
const payload = response.payload;
|
|
12436
|
+
if (payload?.type === "hello-error") {
|
|
12437
|
+
const msg = payload.message ?? "unknown error";
|
|
12438
|
+
throw new Error(`hello-error: ${msg}`);
|
|
12439
|
+
}
|
|
12440
|
+
if (payload?.type !== "hello-ok") {
|
|
12441
|
+
throw new Error(`Unexpected payload type: ${String(payload?.type ?? "missing")}`);
|
|
12442
|
+
}
|
|
12443
|
+
const policy = payload.policy ?? {};
|
|
12444
|
+
const methods = payload.methods ?? [];
|
|
12445
|
+
const events = payload.events ?? [];
|
|
12446
|
+
return {
|
|
12447
|
+
methods: new Set(methods),
|
|
12448
|
+
events: new Set(events),
|
|
12449
|
+
tickIntervalMs: policy.tickIntervalMs ?? 15e3,
|
|
12450
|
+
snapshot: payload.snapshot
|
|
12451
|
+
};
|
|
12452
|
+
}
|
|
12453
|
+
function getPendingRequests() {
|
|
12454
|
+
const g = globalThis;
|
|
12455
|
+
const hot = g.__cohort_sync__ ?? (g.__cohort_sync__ = {});
|
|
12456
|
+
if (!hot.pendingGatewayRequests) hot.pendingGatewayRequests = /* @__PURE__ */ new Map();
|
|
12457
|
+
return hot.pendingGatewayRequests;
|
|
12458
|
+
}
|
|
12459
|
+
var GatewayClient2 = class {
|
|
12460
|
+
port;
|
|
12461
|
+
logger;
|
|
12462
|
+
ws = null;
|
|
12463
|
+
alive = false;
|
|
12464
|
+
tickWatchdog = null;
|
|
12465
|
+
reconnectTimer = null;
|
|
12466
|
+
reconnectAttempts = 0;
|
|
12467
|
+
closed = false;
|
|
12468
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
12469
|
+
pluginVersion;
|
|
12470
|
+
tickIntervalMs = 15e3;
|
|
12471
|
+
// default; overwritten by hello-ok response
|
|
12472
|
+
deviceIdentity;
|
|
12473
|
+
/** Public Sets populated from hello-ok — consumers can inspect gateway capabilities */
|
|
12474
|
+
availableMethods = /* @__PURE__ */ new Set();
|
|
12475
|
+
availableEvents = /* @__PURE__ */ new Set();
|
|
12476
|
+
/**
|
|
12477
|
+
* @param port - Gateway WebSocket port
|
|
12478
|
+
* @param token - Auth token (stored in closure via constructor param, NOT on this or globalThis)
|
|
12479
|
+
* @param logger - RuntimeLogger from plugin SDK
|
|
12480
|
+
* @param pluginVersion - Version string for the connect frame userAgent
|
|
12481
|
+
*/
|
|
12482
|
+
constructor(port, token, logger, pluginVersion = "0.5.0") {
|
|
12483
|
+
this.port = port;
|
|
12484
|
+
this.logger = logger;
|
|
12485
|
+
this.pluginVersion = pluginVersion;
|
|
12486
|
+
this.deviceIdentity = loadOrCreateDeviceIdentity();
|
|
12487
|
+
this._getToken = () => token;
|
|
12488
|
+
}
|
|
12489
|
+
/** Token accessor — closure over constructor param */
|
|
12490
|
+
_getToken;
|
|
12491
|
+
/**
|
|
12492
|
+
* Connect to the gateway, perform the protocol v3 handshake, and start
|
|
12493
|
+
* the tick watchdog.
|
|
12494
|
+
*
|
|
12495
|
+
* Flow:
|
|
12496
|
+
* 1. Open WebSocket to ws://127.0.0.1:{port}
|
|
12497
|
+
* 2. Wait up to 500ms for connect.challenge event (optional)
|
|
12498
|
+
* 3. Send connect request frame
|
|
12499
|
+
* 4. Wait for hello-ok response
|
|
12500
|
+
* 5. Start tick watchdog at 2.5x tickIntervalMs
|
|
12501
|
+
*/
|
|
12502
|
+
async connect() {
|
|
12503
|
+
if (this.closed) throw new Error("GatewayClient has been closed");
|
|
12504
|
+
diag("GW_CLIENT_CONNECTING", { port: this.port });
|
|
12505
|
+
return new Promise((resolve, reject) => {
|
|
12506
|
+
const ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
|
|
12507
|
+
this.ws = ws;
|
|
12508
|
+
let settled = false;
|
|
12509
|
+
const settle = (err) => {
|
|
12510
|
+
if (settled) return;
|
|
12511
|
+
settled = true;
|
|
12512
|
+
if (err) {
|
|
12513
|
+
this.alive = false;
|
|
12514
|
+
reject(err);
|
|
12515
|
+
} else {
|
|
12516
|
+
resolve();
|
|
12517
|
+
}
|
|
12518
|
+
};
|
|
12519
|
+
const handshakeTimeout = setTimeout(() => {
|
|
12520
|
+
settle(new Error("Gateway handshake timed out after 10000ms"));
|
|
12521
|
+
ws.close();
|
|
12522
|
+
}, 1e4);
|
|
12523
|
+
ws.addEventListener("open", () => {
|
|
12524
|
+
diag("GW_CLIENT_WS_OPEN", { port: this.port });
|
|
12525
|
+
let challengeReceived = false;
|
|
12526
|
+
let challengeNonce = "";
|
|
12527
|
+
const challengeTimer = setTimeout(() => {
|
|
12528
|
+
if (!challengeReceived) {
|
|
12529
|
+
diag("GW_CLIENT_NO_CHALLENGE", { waited: 500 });
|
|
12530
|
+
sendConnect();
|
|
12531
|
+
}
|
|
12532
|
+
}, 500);
|
|
12533
|
+
const onChallengeMessage = (event) => {
|
|
12534
|
+
try {
|
|
12535
|
+
const data = JSON.parse(String(event.data));
|
|
12536
|
+
if (data.type === "event" && data.event === "connect.challenge") {
|
|
12537
|
+
challengeReceived = true;
|
|
12538
|
+
clearTimeout(challengeTimer);
|
|
12539
|
+
challengeNonce = data.payload?.nonce ?? "";
|
|
12540
|
+
diag("GW_CLIENT_CHALLENGE_RECEIVED", { hasNonce: !!challengeNonce });
|
|
12541
|
+
sendConnect();
|
|
12542
|
+
}
|
|
12543
|
+
} catch {
|
|
12544
|
+
}
|
|
12545
|
+
};
|
|
12546
|
+
ws.addEventListener("message", onChallengeMessage);
|
|
12547
|
+
const sendConnect = () => {
|
|
12548
|
+
ws.removeEventListener("message", onChallengeMessage);
|
|
12549
|
+
const id = crypto2.randomUUID();
|
|
12550
|
+
const frame = buildConnectFrame(
|
|
12551
|
+
id,
|
|
12552
|
+
this._getToken(),
|
|
12553
|
+
this.pluginVersion,
|
|
12554
|
+
this.deviceIdentity,
|
|
12555
|
+
challengeNonce
|
|
12556
|
+
);
|
|
12557
|
+
diag("GW_CLIENT_SENDING_CONNECT", { id, protocol: 3 });
|
|
12558
|
+
ws.send(JSON.stringify(frame));
|
|
12559
|
+
const onHelloMessage = (event) => {
|
|
12560
|
+
try {
|
|
12561
|
+
const data = JSON.parse(String(event.data));
|
|
12562
|
+
if (data.type === "res" && data.id === id) {
|
|
12563
|
+
ws.removeEventListener("message", onHelloMessage);
|
|
12564
|
+
clearTimeout(handshakeTimeout);
|
|
12565
|
+
try {
|
|
12566
|
+
const result = parseHelloOk(data);
|
|
12567
|
+
this.availableMethods = result.methods;
|
|
12568
|
+
this.availableEvents = result.events;
|
|
12569
|
+
this.tickIntervalMs = result.tickIntervalMs;
|
|
12570
|
+
this.alive = true;
|
|
12571
|
+
this.reconnectAttempts = 0;
|
|
12572
|
+
diag("GW_CLIENT_HELLO_OK", {
|
|
12573
|
+
methods: result.methods.size,
|
|
12574
|
+
events: result.events.size,
|
|
12575
|
+
tickIntervalMs: result.tickIntervalMs
|
|
12576
|
+
});
|
|
12577
|
+
this.startTickWatchdog(result.tickIntervalMs);
|
|
12578
|
+
ws.addEventListener("message", (ev) => this.handleMessage(ev));
|
|
12579
|
+
if (result.snapshot) {
|
|
12580
|
+
this.emit("snapshot", result.snapshot);
|
|
12581
|
+
}
|
|
12582
|
+
this.logger.info("cohort-sync: gateway client connected (protocol v3)");
|
|
12583
|
+
settle();
|
|
12584
|
+
} catch (err) {
|
|
12585
|
+
diag("GW_CLIENT_HELLO_FAILED", { error: String(err) });
|
|
12586
|
+
settle(err instanceof Error ? err : new Error(String(err)));
|
|
12587
|
+
ws.close();
|
|
12588
|
+
}
|
|
12589
|
+
}
|
|
12590
|
+
} catch {
|
|
12591
|
+
}
|
|
12592
|
+
};
|
|
12593
|
+
ws.addEventListener("message", onHelloMessage);
|
|
12594
|
+
};
|
|
12595
|
+
});
|
|
12596
|
+
ws.addEventListener("close", () => {
|
|
12597
|
+
clearTimeout(handshakeTimeout);
|
|
12598
|
+
this.alive = false;
|
|
12599
|
+
this.stopTickWatchdog();
|
|
12600
|
+
diag("GW_CLIENT_WS_CLOSED", { port: this.port });
|
|
12601
|
+
this.logger.warn("cohort-sync: gateway client WebSocket closed");
|
|
12602
|
+
const pending = getPendingRequests();
|
|
12603
|
+
for (const [, entry] of pending) {
|
|
12604
|
+
clearTimeout(entry.timer);
|
|
12605
|
+
entry.reject(new Error("Gateway WebSocket closed"));
|
|
12606
|
+
}
|
|
12607
|
+
pending.clear();
|
|
12608
|
+
if (!settled) {
|
|
12609
|
+
settle(new Error("Gateway WebSocket closed during handshake"));
|
|
12610
|
+
}
|
|
12611
|
+
if (!this.closed) {
|
|
12612
|
+
this.scheduleReconnect();
|
|
12613
|
+
}
|
|
12379
12614
|
});
|
|
12615
|
+
ws.addEventListener("error", (err) => {
|
|
12616
|
+
diag("GW_CLIENT_WS_ERROR", { error: String(err) });
|
|
12617
|
+
this.logger.error(`cohort-sync: gateway client WS error: ${String(err)}`);
|
|
12618
|
+
});
|
|
12619
|
+
});
|
|
12620
|
+
}
|
|
12621
|
+
/**
|
|
12622
|
+
* Whether the WebSocket is open and the handshake completed successfully.
|
|
12623
|
+
*/
|
|
12624
|
+
isAlive() {
|
|
12625
|
+
return this.alive && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
12626
|
+
}
|
|
12627
|
+
/**
|
|
12628
|
+
* Register an event handler for gateway events (tick, cron.changed, etc.).
|
|
12629
|
+
*/
|
|
12630
|
+
on(event, handler) {
|
|
12631
|
+
let handlers = this.eventHandlers.get(event);
|
|
12632
|
+
if (!handlers) {
|
|
12633
|
+
handlers = /* @__PURE__ */ new Set();
|
|
12634
|
+
this.eventHandlers.set(event, handlers);
|
|
12380
12635
|
}
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12636
|
+
handlers.add(handler);
|
|
12637
|
+
}
|
|
12638
|
+
/**
|
|
12639
|
+
* Send a request to the gateway and wait for the response.
|
|
12640
|
+
*
|
|
12641
|
+
* Validates the method against ALLOWED_METHODS to prevent accidentally
|
|
12642
|
+
* calling admin or unauthorized methods.
|
|
12643
|
+
*/
|
|
12644
|
+
async request(method, params, timeoutMs = 1e4) {
|
|
12645
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
12646
|
+
throw new Error(`Method "${method}" is not in ALLOWED_METHODS`);
|
|
12647
|
+
}
|
|
12648
|
+
if (!this.isAlive()) {
|
|
12649
|
+
throw new Error("Gateway client is not connected");
|
|
12650
|
+
}
|
|
12651
|
+
const id = crypto2.randomUUID();
|
|
12652
|
+
const frame = {
|
|
12653
|
+
type: "req",
|
|
12654
|
+
id,
|
|
12655
|
+
method,
|
|
12656
|
+
params
|
|
12657
|
+
};
|
|
12658
|
+
const pending = getPendingRequests();
|
|
12659
|
+
return new Promise((resolve, reject) => {
|
|
12660
|
+
const timer = setTimeout(() => {
|
|
12661
|
+
pending.delete(id);
|
|
12662
|
+
reject(new Error(`Gateway method "${method}" timed out after ${timeoutMs}ms`));
|
|
12663
|
+
}, timeoutMs);
|
|
12664
|
+
pending.set(id, {
|
|
12665
|
+
resolve,
|
|
12666
|
+
reject,
|
|
12667
|
+
timer
|
|
12390
12668
|
});
|
|
12669
|
+
this.ws.send(JSON.stringify(frame));
|
|
12670
|
+
});
|
|
12671
|
+
}
|
|
12672
|
+
/**
|
|
12673
|
+
* Clean shutdown — close WebSocket, clear timers, reject pending requests.
|
|
12674
|
+
*/
|
|
12675
|
+
close() {
|
|
12676
|
+
this.closed = true;
|
|
12677
|
+
this.alive = false;
|
|
12678
|
+
this.stopTickWatchdog();
|
|
12679
|
+
if (this.reconnectTimer) {
|
|
12680
|
+
clearTimeout(this.reconnectTimer);
|
|
12681
|
+
this.reconnectTimer = null;
|
|
12391
12682
|
}
|
|
12392
|
-
|
|
12393
|
-
|
|
12683
|
+
const pending = getPendingRequests();
|
|
12684
|
+
for (const [, entry] of pending) {
|
|
12685
|
+
clearTimeout(entry.timer);
|
|
12686
|
+
entry.reject(new Error("Gateway client closed"));
|
|
12687
|
+
}
|
|
12688
|
+
pending.clear();
|
|
12689
|
+
if (this.ws) {
|
|
12690
|
+
this.ws.close();
|
|
12691
|
+
this.ws = null;
|
|
12692
|
+
}
|
|
12693
|
+
diag("GW_CLIENT_CLOSED", { port: this.port });
|
|
12694
|
+
this.logger.info("cohort-sync: gateway client closed");
|
|
12695
|
+
}
|
|
12696
|
+
// -------------------------------------------------------------------------
|
|
12697
|
+
// Private methods
|
|
12698
|
+
// -------------------------------------------------------------------------
|
|
12699
|
+
/**
|
|
12700
|
+
* Route incoming WebSocket messages to the appropriate handler.
|
|
12701
|
+
*
|
|
12702
|
+
* Frame types:
|
|
12703
|
+
* - "res" → resolve/reject a pending request
|
|
12704
|
+
* - "event" → dispatch to registered event handlers; reset tick watchdog on tick
|
|
12705
|
+
*/
|
|
12706
|
+
handleMessage(event) {
|
|
12394
12707
|
try {
|
|
12395
|
-
const
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12708
|
+
const data = JSON.parse(String(event.data));
|
|
12709
|
+
if (data.type === "res") {
|
|
12710
|
+
this.handleResponse(data);
|
|
12711
|
+
} else if (data.type === "event") {
|
|
12712
|
+
this.handleEvent(data);
|
|
12713
|
+
}
|
|
12714
|
+
} catch {
|
|
12402
12715
|
}
|
|
12403
|
-
}
|
|
12404
|
-
|
|
12716
|
+
}
|
|
12717
|
+
handleResponse(frame) {
|
|
12718
|
+
const pending = getPendingRequests();
|
|
12719
|
+
const entry = pending.get(frame.id);
|
|
12720
|
+
if (!entry) return;
|
|
12721
|
+
pending.delete(frame.id);
|
|
12722
|
+
clearTimeout(entry.timer);
|
|
12723
|
+
if (frame.ok) {
|
|
12724
|
+
entry.resolve(frame.payload);
|
|
12725
|
+
} else {
|
|
12726
|
+
entry.reject(new Error(`Gateway method failed: ${frame.error?.message ?? "unknown error"}`));
|
|
12727
|
+
}
|
|
12728
|
+
}
|
|
12729
|
+
handleEvent(frame) {
|
|
12730
|
+
if (frame.event === "tick") {
|
|
12731
|
+
this.resetTickWatchdog();
|
|
12732
|
+
}
|
|
12733
|
+
this.emit(frame.event, frame.payload);
|
|
12734
|
+
}
|
|
12735
|
+
emit(event, payload) {
|
|
12736
|
+
const handlers = this.eventHandlers.get(event);
|
|
12737
|
+
if (handlers) {
|
|
12738
|
+
for (const handler of handlers) {
|
|
12739
|
+
try {
|
|
12740
|
+
handler(payload);
|
|
12741
|
+
} catch (err) {
|
|
12742
|
+
this.logger.error(`cohort-sync: event handler error for "${event}": ${String(err)}`);
|
|
12743
|
+
}
|
|
12744
|
+
}
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
/**
|
|
12748
|
+
* Start the tick watchdog timer.
|
|
12749
|
+
*
|
|
12750
|
+
* The gateway sends tick events at tickIntervalMs. If we don't receive
|
|
12751
|
+
* one within 2.5x that interval, the connection is considered dead.
|
|
12752
|
+
*/
|
|
12753
|
+
startTickWatchdog(tickIntervalMs) {
|
|
12754
|
+
this.stopTickWatchdog();
|
|
12755
|
+
const watchdogMs = Math.round(tickIntervalMs * 2.5);
|
|
12756
|
+
this.tickWatchdog = setTimeout(() => {
|
|
12757
|
+
diag("GW_CLIENT_TICK_TIMEOUT", { watchdogMs });
|
|
12758
|
+
this.logger.warn(`cohort-sync: tick watchdog expired after ${watchdogMs}ms \u2014 closing connection`);
|
|
12759
|
+
this.alive = false;
|
|
12760
|
+
this.ws?.close();
|
|
12761
|
+
}, watchdogMs);
|
|
12762
|
+
}
|
|
12763
|
+
resetTickWatchdog() {
|
|
12764
|
+
if (this.tickWatchdog) {
|
|
12765
|
+
this.startTickWatchdog(this.tickIntervalMs);
|
|
12766
|
+
}
|
|
12767
|
+
}
|
|
12768
|
+
stopTickWatchdog() {
|
|
12769
|
+
if (this.tickWatchdog) {
|
|
12770
|
+
clearTimeout(this.tickWatchdog);
|
|
12771
|
+
this.tickWatchdog = null;
|
|
12772
|
+
}
|
|
12773
|
+
}
|
|
12774
|
+
/**
|
|
12775
|
+
* Schedule a reconnection attempt with exponential backoff and jitter.
|
|
12776
|
+
*
|
|
12777
|
+
* Backoff: 1s base, doubles each attempt, capped at 30s.
|
|
12778
|
+
* Jitter: +/- 25% randomization to avoid thundering herd.
|
|
12779
|
+
*/
|
|
12780
|
+
scheduleReconnect() {
|
|
12781
|
+
if (this.closed) return;
|
|
12782
|
+
const baseMs = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
12783
|
+
const jitter = baseMs * 0.25 * (Math.random() * 2 - 1);
|
|
12784
|
+
const delayMs = Math.round(Math.max(baseMs + jitter, 1e3));
|
|
12785
|
+
this.reconnectAttempts++;
|
|
12786
|
+
diag("GW_CLIENT_RECONNECT_SCHEDULED", {
|
|
12787
|
+
attempt: this.reconnectAttempts,
|
|
12788
|
+
delayMs
|
|
12789
|
+
});
|
|
12790
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
12791
|
+
this.reconnectTimer = null;
|
|
12792
|
+
try {
|
|
12793
|
+
diag("GW_CLIENT_RECONNECTING", { attempt: this.reconnectAttempts });
|
|
12794
|
+
await this.connect();
|
|
12795
|
+
diag("GW_CLIENT_RECONNECTED", { attempt: this.reconnectAttempts });
|
|
12796
|
+
} catch (err) {
|
|
12797
|
+
diag("GW_CLIENT_RECONNECT_FAILED", {
|
|
12798
|
+
attempt: this.reconnectAttempts,
|
|
12799
|
+
error: String(err)
|
|
12800
|
+
});
|
|
12801
|
+
}
|
|
12802
|
+
}, delayMs);
|
|
12803
|
+
}
|
|
12804
|
+
};
|
|
12405
12805
|
|
|
12406
12806
|
// src/agent-state.ts
|
|
12407
12807
|
import { basename } from "node:path";
|
|
@@ -12902,20 +13302,6 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
|
12902
13302
|
|
|
12903
13303
|
// src/hooks.ts
|
|
12904
13304
|
var BUILD_ID = "B9-ACCOUNTID-20260311";
|
|
12905
|
-
var DIAG_LOG_PATH = "/tmp/cohort-sync-diag.log";
|
|
12906
|
-
function diag(label, data) {
|
|
12907
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
12908
|
-
const payload = data ? " " + JSON.stringify(data, (_k, v2) => {
|
|
12909
|
-
if (typeof v2 === "string" && v2.length > 200) return v2.slice(0, 200) + "\u2026";
|
|
12910
|
-
return v2;
|
|
12911
|
-
}) : "";
|
|
12912
|
-
const line = `[${ts}] ${label}${payload}
|
|
12913
|
-
`;
|
|
12914
|
-
try {
|
|
12915
|
-
fs.appendFileSync(DIAG_LOG_PATH, line);
|
|
12916
|
-
} catch {
|
|
12917
|
-
}
|
|
12918
|
-
}
|
|
12919
13305
|
function dumpCtx(ctx) {
|
|
12920
13306
|
if (!ctx || typeof ctx !== "object") return { _raw: String(ctx) };
|
|
12921
13307
|
const out = {};
|
|
@@ -12940,16 +13326,64 @@ function dumpEvent(event) {
|
|
|
12940
13326
|
}
|
|
12941
13327
|
var PLUGIN_VERSION = "unknown";
|
|
12942
13328
|
try {
|
|
12943
|
-
const pkgPath =
|
|
12944
|
-
const pkgJson = JSON.parse(
|
|
13329
|
+
const pkgPath = path3.join(path3.dirname(new URL(import.meta.url).pathname), "package.json");
|
|
13330
|
+
const pkgJson = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
|
|
12945
13331
|
PLUGIN_VERSION = pkgJson.version ?? "unknown";
|
|
12946
13332
|
} catch {
|
|
12947
13333
|
}
|
|
12948
13334
|
diag("MODULE_LOADED", { BUILD_ID, PLUGIN_VERSION });
|
|
13335
|
+
var lastCronSnapshotJson = "";
|
|
13336
|
+
function resolveGatewayToken(api) {
|
|
13337
|
+
const rawToken = api.config?.gateway?.auth?.token;
|
|
13338
|
+
if (typeof rawToken === "string") return rawToken;
|
|
13339
|
+
if (rawToken && typeof rawToken === "object" && rawToken.source === "env") {
|
|
13340
|
+
return process.env[rawToken.id] ?? null;
|
|
13341
|
+
}
|
|
13342
|
+
return null;
|
|
13343
|
+
}
|
|
13344
|
+
async function quickCronSync(port, token, cfg, resolveAgentName, logger) {
|
|
13345
|
+
const client2 = new GatewayClient2(port, token, logger, PLUGIN_VERSION);
|
|
13346
|
+
try {
|
|
13347
|
+
await client2.connect();
|
|
13348
|
+
const result = await client2.request("cron.list", { includeDisabled: true });
|
|
13349
|
+
diag("GW_CLIENT_CRON_LIST_RESULT", { type: typeof result, isArray: Array.isArray(result), keys: result && typeof result === "object" ? Object.keys(result) : [], length: Array.isArray(result) ? result.length : void 0 });
|
|
13350
|
+
const jobs = Array.isArray(result) ? result : result?.jobs ?? [];
|
|
13351
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13352
|
+
const serialized = JSON.stringify(mapped);
|
|
13353
|
+
if (serialized !== lastCronSnapshotJson) {
|
|
13354
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13355
|
+
lastCronSnapshotJson = serialized;
|
|
13356
|
+
diag("HEARTBEAT_CRON_PUSHED", { count: mapped.length });
|
|
13357
|
+
} else {
|
|
13358
|
+
diag("HEARTBEAT_CRON_UNCHANGED", {});
|
|
13359
|
+
}
|
|
13360
|
+
} finally {
|
|
13361
|
+
client2.close();
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
13364
|
+
function registerCronEventHandlers(client2, cfg, resolveAgentName) {
|
|
13365
|
+
if (client2.availableEvents.has("cron")) {
|
|
13366
|
+
let debounceTimer = null;
|
|
13367
|
+
client2.on("cron", () => {
|
|
13368
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
13369
|
+
debounceTimer = setTimeout(async () => {
|
|
13370
|
+
try {
|
|
13371
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13372
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13373
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13374
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13375
|
+
diag("CRON_EVENT_PUSHED", { count: mapped.length });
|
|
13376
|
+
} catch (err) {
|
|
13377
|
+
diag("CRON_EVENT_PUSH_FAILED", { error: String(err) });
|
|
13378
|
+
}
|
|
13379
|
+
}, 2e3);
|
|
13380
|
+
});
|
|
13381
|
+
}
|
|
13382
|
+
}
|
|
12949
13383
|
function parseIdentityFile(workspaceDir) {
|
|
12950
13384
|
try {
|
|
12951
|
-
const filePath =
|
|
12952
|
-
const content =
|
|
13385
|
+
const filePath = path3.join(workspaceDir, "IDENTITY.md");
|
|
13386
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
12953
13387
|
const identity = {};
|
|
12954
13388
|
for (const line of content.split(/\r?\n/)) {
|
|
12955
13389
|
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
@@ -12987,8 +13421,8 @@ function getOrCreateTracker() {
|
|
|
12987
13421
|
state.tracker = fresh;
|
|
12988
13422
|
return fresh;
|
|
12989
13423
|
}
|
|
12990
|
-
var STATE_FILE_PATH =
|
|
12991
|
-
|
|
13424
|
+
var STATE_FILE_PATH = path3.join(
|
|
13425
|
+
path3.dirname(new URL(import.meta.url).pathname),
|
|
12992
13426
|
".session-state.json"
|
|
12993
13427
|
);
|
|
12994
13428
|
function saveSessionsToDisk(tracker) {
|
|
@@ -13005,14 +13439,14 @@ function saveSessionsToDisk(tracker) {
|
|
|
13005
13439
|
data.sessions.push({ agentName: name, key });
|
|
13006
13440
|
}
|
|
13007
13441
|
}
|
|
13008
|
-
|
|
13442
|
+
fs3.writeFileSync(STATE_FILE_PATH, JSON.stringify(data));
|
|
13009
13443
|
} catch {
|
|
13010
13444
|
}
|
|
13011
13445
|
}
|
|
13012
13446
|
function loadSessionsFromDisk(tracker, logger) {
|
|
13013
13447
|
try {
|
|
13014
|
-
if (!
|
|
13015
|
-
const data = JSON.parse(
|
|
13448
|
+
if (!fs3.existsSync(STATE_FILE_PATH)) return;
|
|
13449
|
+
const data = JSON.parse(fs3.readFileSync(STATE_FILE_PATH, "utf8"));
|
|
13016
13450
|
if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
|
|
13017
13451
|
logger.info("cohort-sync: disk session state too old (>24h), skipping");
|
|
13018
13452
|
return;
|
|
@@ -13059,7 +13493,6 @@ function registerHooks(api, cfg) {
|
|
|
13059
13493
|
const tracker = getOrCreateTracker();
|
|
13060
13494
|
let heartbeatInterval = null;
|
|
13061
13495
|
let activityFlushInterval = null;
|
|
13062
|
-
registerCronGatewayMethods(api);
|
|
13063
13496
|
logger.info(`cohort-sync: registerHooks [${BUILD_ID}]`);
|
|
13064
13497
|
diag("REGISTER_HOOKS", {
|
|
13065
13498
|
BUILD_ID,
|
|
@@ -13200,6 +13633,19 @@ function registerHooks(api, cfg) {
|
|
|
13200
13633
|
const unsub = initCommandSubscription(cfg, logger, resolveAgentName);
|
|
13201
13634
|
hotState.commandSubscription = unsub;
|
|
13202
13635
|
}
|
|
13636
|
+
{
|
|
13637
|
+
const port = api.config?.gateway?.port;
|
|
13638
|
+
const token = resolveGatewayToken(api);
|
|
13639
|
+
if (port && token) {
|
|
13640
|
+
const hotState = getHotState();
|
|
13641
|
+
hotState.gatewayPort = port;
|
|
13642
|
+
hotState.gatewayToken = token;
|
|
13643
|
+
diag("REGISTER_HOOKS_CRON_SYNC", { port });
|
|
13644
|
+
quickCronSync(port, token, cfg, resolveAgentName, logger).catch((err) => {
|
|
13645
|
+
diag("REGISTER_HOOKS_CRON_SYNC_FAILED", { error: String(err) });
|
|
13646
|
+
});
|
|
13647
|
+
}
|
|
13648
|
+
}
|
|
13203
13649
|
api.registerTool((toolCtx) => {
|
|
13204
13650
|
const agentId = toolCtx.agentId ?? "main";
|
|
13205
13651
|
const agentName = resolveAgentName(agentId);
|
|
@@ -13349,18 +13795,6 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13349
13795
|
}
|
|
13350
13796
|
}
|
|
13351
13797
|
saveSessionsToDisk(tracker);
|
|
13352
|
-
const ws = getHotState().gatewayWs;
|
|
13353
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
13354
|
-
try {
|
|
13355
|
-
const jobs = await callGatewayMethod(ws, "cohort-sync/cron-list");
|
|
13356
|
-
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13357
|
-
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13358
|
-
} catch (err) {
|
|
13359
|
-
logger.warn(`cohort-sync: cron list failed: ${String(err)}`);
|
|
13360
|
-
}
|
|
13361
|
-
} else {
|
|
13362
|
-
logger.warn("cohort-sync: gateway WS not connected \u2014 skipping cron snapshot");
|
|
13363
|
-
}
|
|
13364
13798
|
logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
|
|
13365
13799
|
}
|
|
13366
13800
|
async function flushActivityBuffer() {
|
|
@@ -13400,20 +13834,28 @@ Do not attempt to make more comments until ${resetAt}.`
|
|
|
13400
13834
|
diag("GATEWAY_START_BRIDGE_AFTER_SEED", { bridge: { ...getHotState().channelAgentBridge } });
|
|
13401
13835
|
const hotState = getHotState();
|
|
13402
13836
|
hotState.gatewayPort = event.port;
|
|
13403
|
-
const
|
|
13404
|
-
if (
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13837
|
+
const token = resolveGatewayToken(api);
|
|
13838
|
+
if (token) {
|
|
13839
|
+
diag("GW_CLIENT_CONNECTING", { port: event.port, hasToken: true });
|
|
13840
|
+
try {
|
|
13841
|
+
const client2 = new GatewayClient2(event.port, token, logger, PLUGIN_VERSION);
|
|
13842
|
+
await client2.connect();
|
|
13843
|
+
hotState.gatewayProtocolClient = client2;
|
|
13844
|
+
registerCronEventHandlers(client2, cfg, resolveAgentName);
|
|
13845
|
+
diag("GW_CLIENT_CONNECTED", { methods: client2.availableMethods.size, events: client2.availableEvents.size });
|
|
13846
|
+
const cronResult = await client2.request("cron.list", { includeDisabled: true });
|
|
13847
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
13848
|
+
const mapped = jobs.map((j) => mapCronJob(j, resolveAgentName));
|
|
13849
|
+
await pushCronSnapshot(cfg.apiKey, mapped);
|
|
13850
|
+
lastCronSnapshotJson = JSON.stringify(mapped);
|
|
13851
|
+
diag("GW_CLIENT_INITIAL_CRON_PUSH", { count: mapped.length });
|
|
13852
|
+
} catch (err) {
|
|
13853
|
+
diag("GW_CLIENT_CONNECT_FAILED", { error: String(err) });
|
|
13854
|
+
logger.error(`cohort-sync: gateway client connect failed: ${String(err)}`);
|
|
13416
13855
|
}
|
|
13856
|
+
} else {
|
|
13857
|
+
diag("GW_CLIENT_NO_TOKEN", {});
|
|
13858
|
+
logger.warn("cohort-sync: no gateway auth token \u2014 cron operations disabled");
|
|
13417
13859
|
}
|
|
13418
13860
|
await initSubscription(
|
|
13419
13861
|
event.port,
|
package/dist/package.json
CHANGED