@cfio/cohort-sync 0.2.3 → 0.3.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 +248 -28
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11503,44 +11503,63 @@ var _systemSchema = defineSchema({
|
|
|
11503
11503
|
});
|
|
11504
11504
|
|
|
11505
11505
|
// src/subscription.ts
|
|
11506
|
+
import { execFileSync } from "node:child_process";
|
|
11506
11507
|
function deriveConvexUrl(apiUrl) {
|
|
11507
11508
|
return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
|
|
11508
11509
|
}
|
|
11510
|
+
var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
|
|
11511
|
+
- Does your planned response address the task's stated scope? If not, do not comment.
|
|
11512
|
+
- Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
|
|
11513
|
+
- If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
|
|
11509
11514
|
function buildNotificationMessage(n) {
|
|
11510
11515
|
let header;
|
|
11511
11516
|
let cta;
|
|
11512
11517
|
switch (n.type) {
|
|
11513
11518
|
case "comment":
|
|
11514
11519
|
if (n.isMentioned) {
|
|
11515
|
-
header =
|
|
11520
|
+
header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
|
|
11516
11521
|
By: ${n.actorName}`;
|
|
11517
11522
|
cta = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
|
|
11518
11523
|
} else {
|
|
11519
|
-
header =
|
|
11524
|
+
header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
|
|
11520
11525
|
From: ${n.actorName}`;
|
|
11521
11526
|
cta = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
|
|
11522
11527
|
}
|
|
11523
11528
|
break;
|
|
11524
11529
|
case "assignment":
|
|
11525
|
-
header =
|
|
11530
|
+
header = `You were assigned to task #${n.taskNumber} "${n.taskTitle}"
|
|
11526
11531
|
By: ${n.actorName}`;
|
|
11527
11532
|
cta = "Review the task description and begin working on it.";
|
|
11528
11533
|
break;
|
|
11529
11534
|
case "status_change":
|
|
11530
|
-
header =
|
|
11535
|
+
header = `Status changed on task #${n.taskNumber} "${n.taskTitle}"
|
|
11531
11536
|
By: ${n.actorName}`;
|
|
11532
11537
|
cta = "Review the status change and take any follow-up action needed.";
|
|
11533
11538
|
break;
|
|
11534
11539
|
default:
|
|
11535
|
-
header =
|
|
11540
|
+
header = `Notification on task #${n.taskNumber} "${n.taskTitle}"
|
|
11536
11541
|
From: ${n.actorName}`;
|
|
11537
11542
|
cta = "Check the task and respond if needed.";
|
|
11538
11543
|
}
|
|
11539
11544
|
const body = n.preview ? `
|
|
11540
11545
|
Comment: "${n.preview}"` : "";
|
|
11541
|
-
|
|
11546
|
+
let scope = "";
|
|
11547
|
+
if (n.taskDescription && n.type === "comment") {
|
|
11548
|
+
const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
|
|
11549
|
+
scope = `
|
|
11542
11550
|
|
|
11543
|
-
${
|
|
11551
|
+
Scope: ${truncated}`;
|
|
11552
|
+
}
|
|
11553
|
+
const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
|
|
11554
|
+
|
|
11555
|
+
${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
|
|
11556
|
+
const promptBlock = n.type === "comment" ? `
|
|
11557
|
+
|
|
11558
|
+
---
|
|
11559
|
+
${prompt}` : "";
|
|
11560
|
+
return `${header}${scope}${body}
|
|
11561
|
+
|
|
11562
|
+
${cta}${promptBlock}`;
|
|
11544
11563
|
}
|
|
11545
11564
|
async function injectNotification(port, hooksToken, n, agentId = "main") {
|
|
11546
11565
|
const response = await fetch(`http://localhost:${port}/hooks/agent`, {
|
|
@@ -11566,6 +11585,10 @@ var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredBy
|
|
|
11566
11585
|
var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
|
|
11567
11586
|
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
11568
11587
|
var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
|
|
11588
|
+
var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
|
|
11589
|
+
var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
|
|
11590
|
+
var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
|
|
11591
|
+
var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
|
|
11569
11592
|
var HOT_KEY = "__cohort_sync__";
|
|
11570
11593
|
function getHotState() {
|
|
11571
11594
|
let state = globalThis[HOT_KEY];
|
|
@@ -11705,6 +11728,101 @@ async function initSubscription(port, cfg, hooksToken, logger) {
|
|
|
11705
11728
|
}
|
|
11706
11729
|
state.unsubscribers = [...unsubscribers];
|
|
11707
11730
|
}
|
|
11731
|
+
function initCommandSubscription(cfg, logger) {
|
|
11732
|
+
const c = getOrCreateClient();
|
|
11733
|
+
if (!c) {
|
|
11734
|
+
logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
|
|
11735
|
+
return;
|
|
11736
|
+
}
|
|
11737
|
+
let processing = false;
|
|
11738
|
+
const unsubscribe = c.onUpdate(
|
|
11739
|
+
getPendingCommandsForPlugin,
|
|
11740
|
+
{ apiKey: cfg.apiKey },
|
|
11741
|
+
async (commands) => {
|
|
11742
|
+
if (processing) return;
|
|
11743
|
+
if (commands.length === 0) return;
|
|
11744
|
+
processing = true;
|
|
11745
|
+
try {
|
|
11746
|
+
for (const cmd of commands) {
|
|
11747
|
+
logger.info(`cohort-sync: processing command: ${cmd.type} (${cmd._id})`);
|
|
11748
|
+
try {
|
|
11749
|
+
await c.mutation(acknowledgeCommandRef, {
|
|
11750
|
+
commandId: cmd._id,
|
|
11751
|
+
apiKey: cfg.apiKey
|
|
11752
|
+
});
|
|
11753
|
+
if (cmd.type === "restart") {
|
|
11754
|
+
logger.info("cohort-sync: restart acknowledged, terminating in 500ms");
|
|
11755
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
11756
|
+
process.kill(process.pid, "SIGTERM");
|
|
11757
|
+
return;
|
|
11758
|
+
}
|
|
11759
|
+
if (cmd.type.startsWith("cron") && cmd.payload?.jobId) {
|
|
11760
|
+
handleCronCommand(cmd.type, cmd.payload.jobId, logger);
|
|
11761
|
+
try {
|
|
11762
|
+
const freshJobs = fetchCronJobs(logger);
|
|
11763
|
+
await pushCronSnapshot(cfg.apiKey, freshJobs);
|
|
11764
|
+
} catch (snapErr) {
|
|
11765
|
+
logger.warn(`cohort-sync: post-command snapshot push failed: ${String(snapErr)}`);
|
|
11766
|
+
}
|
|
11767
|
+
} else if (cmd.type.startsWith("cron")) {
|
|
11768
|
+
logger.warn(`cohort-sync: cron command missing jobId: ${cmd.type}`);
|
|
11769
|
+
} else {
|
|
11770
|
+
logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
|
|
11771
|
+
}
|
|
11772
|
+
} catch (err) {
|
|
11773
|
+
logger.error(`cohort-sync: failed to process command ${cmd._id}: ${String(err)}`);
|
|
11774
|
+
}
|
|
11775
|
+
}
|
|
11776
|
+
} finally {
|
|
11777
|
+
processing = false;
|
|
11778
|
+
}
|
|
11779
|
+
},
|
|
11780
|
+
(err) => {
|
|
11781
|
+
logger.error(`cohort-sync: command subscription error: ${String(err)}`);
|
|
11782
|
+
}
|
|
11783
|
+
);
|
|
11784
|
+
unsubscribers.push(unsubscribe);
|
|
11785
|
+
getHotState().unsubscribers = [...unsubscribers];
|
|
11786
|
+
logger.info("cohort-sync: command subscription active");
|
|
11787
|
+
}
|
|
11788
|
+
function handleCronCommand(type, jobId, logger) {
|
|
11789
|
+
const subcommandMap = {
|
|
11790
|
+
cronEnable: ["cron", "enable", jobId],
|
|
11791
|
+
cronDisable: ["cron", "disable", jobId],
|
|
11792
|
+
cronRunNow: ["cron", "run", jobId],
|
|
11793
|
+
cronDelete: ["cron", "delete", jobId, "--force"]
|
|
11794
|
+
};
|
|
11795
|
+
const args = subcommandMap[type];
|
|
11796
|
+
if (!args) {
|
|
11797
|
+
logger.warn(`cohort-sync: unknown cron command type: ${type}`);
|
|
11798
|
+
return;
|
|
11799
|
+
}
|
|
11800
|
+
const timeout = type === "cronRunNow" ? 3e4 : 15e3;
|
|
11801
|
+
try {
|
|
11802
|
+
execFileSync("openclaw", args, {
|
|
11803
|
+
encoding: "utf8",
|
|
11804
|
+
timeout,
|
|
11805
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11806
|
+
});
|
|
11807
|
+
logger.info(`cohort-sync: cron command ${type} executed for job ${jobId}`);
|
|
11808
|
+
} catch (err) {
|
|
11809
|
+
logger.error(`cohort-sync: cron command ${type} failed for job ${jobId}: ${String(err)}`);
|
|
11810
|
+
throw err;
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
async function callAddCommentFromPlugin(apiKey, args) {
|
|
11814
|
+
const c = getOrCreateClient();
|
|
11815
|
+
if (!c) {
|
|
11816
|
+
throw new Error("Convex client not initialized \u2014 subscription may not be active");
|
|
11817
|
+
}
|
|
11818
|
+
return await c.mutation(addCommentFromPluginRef, {
|
|
11819
|
+
apiKey,
|
|
11820
|
+
taskNumber: args.taskNumber,
|
|
11821
|
+
agentName: args.agentName,
|
|
11822
|
+
content: args.content,
|
|
11823
|
+
noReply: args.noReply
|
|
11824
|
+
});
|
|
11825
|
+
}
|
|
11708
11826
|
function closeSubscription() {
|
|
11709
11827
|
for (const unsub of unsubscribers) {
|
|
11710
11828
|
try {
|
|
@@ -11752,6 +11870,15 @@ async function pushActivity(apiKey, entries) {
|
|
|
11752
11870
|
getLogger().error(`cohort-sync: pushActivity failed: ${err}`);
|
|
11753
11871
|
}
|
|
11754
11872
|
}
|
|
11873
|
+
async function pushCronSnapshot(apiKey, jobs) {
|
|
11874
|
+
const c = getOrCreateClient();
|
|
11875
|
+
if (!c) return;
|
|
11876
|
+
try {
|
|
11877
|
+
await c.mutation(upsertCronSnapshotFromPluginRef, { apiKey, jobs });
|
|
11878
|
+
} catch (err) {
|
|
11879
|
+
getLogger().error(`cohort-sync: pushCronSnapshot failed: ${err}`);
|
|
11880
|
+
}
|
|
11881
|
+
}
|
|
11755
11882
|
function saveIntervalsToHot(heartbeat, activityFlush) {
|
|
11756
11883
|
const state = getHotState();
|
|
11757
11884
|
state.intervals = { heartbeat, activityFlush };
|
|
@@ -11815,6 +11942,44 @@ function fetchSkills(logger) {
|
|
|
11815
11942
|
return [];
|
|
11816
11943
|
}
|
|
11817
11944
|
}
|
|
11945
|
+
function fetchCronJobs(logger) {
|
|
11946
|
+
try {
|
|
11947
|
+
const raw = execSync("openclaw cron list --all --json", {
|
|
11948
|
+
encoding: "utf8",
|
|
11949
|
+
timeout: 1e4,
|
|
11950
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11951
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
11952
|
+
});
|
|
11953
|
+
const parsed = JSON.parse(extractJson(raw));
|
|
11954
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.jobs ?? [];
|
|
11955
|
+
return list.map((j) => ({
|
|
11956
|
+
id: String(j.jobId ?? j.id ?? "unknown"),
|
|
11957
|
+
text: String(j.name ?? j.text ?? ""),
|
|
11958
|
+
schedule: formatSchedule(j.schedule),
|
|
11959
|
+
...j.nextRunAt != null ? { nextRun: Number(j.nextRunAt) } : {},
|
|
11960
|
+
...j.lastRunAt != null ? { lastRun: Number(j.lastRunAt) } : {},
|
|
11961
|
+
...j.lastStatus ? { lastStatus: String(j.lastStatus) } : {},
|
|
11962
|
+
enabled: j.enabled !== false,
|
|
11963
|
+
...j.agentId != null ? { agentId: String(j.agentId) } : {}
|
|
11964
|
+
}));
|
|
11965
|
+
} catch (err) {
|
|
11966
|
+
logger.warn(`cohort-sync: failed to fetch cron jobs: ${String(err)}`);
|
|
11967
|
+
return [];
|
|
11968
|
+
}
|
|
11969
|
+
}
|
|
11970
|
+
function formatSchedule(schedule) {
|
|
11971
|
+
if (typeof schedule === "string") return schedule;
|
|
11972
|
+
if (schedule && typeof schedule === "object") {
|
|
11973
|
+
const s = schedule;
|
|
11974
|
+
if (s.kind === "cron" && typeof s.expr === "string") return s.expr;
|
|
11975
|
+
if (s.kind === "interval" && typeof s.everyMs === "number") {
|
|
11976
|
+
const mins = Math.floor(Number(s.everyMs) / 6e4);
|
|
11977
|
+
if (mins > 0) return `*/${mins} * * * *`;
|
|
11978
|
+
return `every ${s.everyMs}ms`;
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
return String(schedule);
|
|
11982
|
+
}
|
|
11818
11983
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
11819
11984
|
function normalizeStatus(status) {
|
|
11820
11985
|
return VALID_STATUSES.has(status) ? status : "idle";
|
|
@@ -12731,31 +12896,75 @@ function registerHooks(api, cfg) {
|
|
|
12731
12896
|
description: "Post a comment on a Cohort task. Use this to respond to @mentions or collaborate on tasks.",
|
|
12732
12897
|
parameters: Type.Object({
|
|
12733
12898
|
task_number: Type.Number({ description: "Task number (e.g. 312)" }),
|
|
12734
|
-
comment: Type.String({ description: "Comment text to post" })
|
|
12899
|
+
comment: Type.String({ description: "Comment text to post" }),
|
|
12900
|
+
no_reply: Type.Optional(Type.Boolean({
|
|
12901
|
+
description: "If true, no notifications will be sent for this comment. Use for final/closing comments."
|
|
12902
|
+
}))
|
|
12735
12903
|
}),
|
|
12736
12904
|
async execute(_toolCallId, params) {
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12905
|
+
try {
|
|
12906
|
+
const result = await callAddCommentFromPlugin(cfg.apiKey, {
|
|
12907
|
+
taskNumber: params.task_number,
|
|
12908
|
+
agentName,
|
|
12909
|
+
content: params.comment,
|
|
12910
|
+
noReply: params.no_reply ?? false
|
|
12911
|
+
});
|
|
12912
|
+
const lines = [`Comment posted on task #${params.task_number}.`];
|
|
12913
|
+
if (result.stats) {
|
|
12914
|
+
lines.push("");
|
|
12915
|
+
lines.push(`This task has ${result.stats.totalComments} comments. ${result.stats.myRecentCount}/${result.stats.threshold} hourly limit used on this task.`);
|
|
12748
12916
|
}
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12917
|
+
if (result.budget) {
|
|
12918
|
+
lines.push(`Daily budget: ${result.budget.used}/${result.budget.limit}`);
|
|
12919
|
+
}
|
|
12920
|
+
return {
|
|
12921
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
12922
|
+
details: result
|
|
12923
|
+
};
|
|
12924
|
+
} catch (err) {
|
|
12925
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12926
|
+
if (msg.includes("AGENT_COMMENTS_LOCKED")) {
|
|
12927
|
+
return {
|
|
12928
|
+
content: [{
|
|
12929
|
+
type: "text",
|
|
12930
|
+
text: `Cannot comment on task #${params.task_number}.
|
|
12931
|
+
Reason: Agent comments are locked on this task.
|
|
12932
|
+
Do not re-attempt to comment on this task.`
|
|
12933
|
+
}],
|
|
12934
|
+
details: { error: "AGENT_COMMENTS_LOCKED", taskNumber: params.task_number }
|
|
12935
|
+
};
|
|
12936
|
+
}
|
|
12937
|
+
if (msg.includes("TASK_HOUR_LIMIT_REACHED")) {
|
|
12938
|
+
const parts = msg.split("|");
|
|
12939
|
+
const count = parts[1] ?? "?";
|
|
12940
|
+
const limit = parts[2] ?? "?";
|
|
12941
|
+
return {
|
|
12942
|
+
content: [{
|
|
12943
|
+
type: "text",
|
|
12944
|
+
text: `Cannot comment on task #${params.task_number}.
|
|
12945
|
+
Reason: You have posted ${count} comments on this task in the last hour (limit: ${limit}).
|
|
12946
|
+
Step back from this task. Do not comment again until the next hour.`
|
|
12947
|
+
}],
|
|
12948
|
+
details: { error: "TASK_HOUR_LIMIT_REACHED", count, limit, taskNumber: params.task_number }
|
|
12949
|
+
};
|
|
12950
|
+
}
|
|
12951
|
+
if (msg.includes("DAILY_LIMIT_REACHED")) {
|
|
12952
|
+
const parts = msg.split("|");
|
|
12953
|
+
const used = parts[1] ?? "?";
|
|
12954
|
+
const max = parts[2] ?? "?";
|
|
12955
|
+
const resetAt = parts[3] ?? "tomorrow";
|
|
12956
|
+
return {
|
|
12957
|
+
content: [{
|
|
12958
|
+
type: "text",
|
|
12959
|
+
text: `Cannot comment on task #${params.task_number}.
|
|
12960
|
+
Reason: Daily comment limit reached (${used}/${max}).
|
|
12961
|
+
Do not attempt to make more comments until ${resetAt}.`
|
|
12962
|
+
}],
|
|
12963
|
+
details: { error: "DAILY_LIMIT_REACHED", used, max, resetAt, taskNumber: params.task_number }
|
|
12964
|
+
};
|
|
12965
|
+
}
|
|
12966
|
+
throw err;
|
|
12753
12967
|
}
|
|
12754
|
-
const result = await res.json();
|
|
12755
|
-
return {
|
|
12756
|
-
content: [{ type: "text", text: `Comment posted on task #${params.task_number}` }],
|
|
12757
|
-
details: result
|
|
12758
|
-
};
|
|
12759
12968
|
}
|
|
12760
12969
|
};
|
|
12761
12970
|
});
|
|
@@ -12813,6 +13022,16 @@ function registerHooks(api, cfg) {
|
|
|
12813
13022
|
}
|
|
12814
13023
|
}
|
|
12815
13024
|
saveSessionsToDisk(tracker);
|
|
13025
|
+
try {
|
|
13026
|
+
const cronJobs2 = fetchCronJobs(logger);
|
|
13027
|
+
const resolvedJobs = cronJobs2.map((job) => ({
|
|
13028
|
+
...job,
|
|
13029
|
+
agentId: job.agentId ? resolveAgentName(job.agentId) : job.agentId
|
|
13030
|
+
}));
|
|
13031
|
+
await pushCronSnapshot(cfg.apiKey, resolvedJobs);
|
|
13032
|
+
} catch (err) {
|
|
13033
|
+
logger.warn(`cohort-sync: heartbeat cron push failed: ${String(err)}`);
|
|
13034
|
+
}
|
|
12816
13035
|
logger.info(`cohort-sync: heartbeat pushed for ${allAgentIds.length} agents`);
|
|
12817
13036
|
}
|
|
12818
13037
|
async function flushActivityBuffer() {
|
|
@@ -12858,6 +13077,7 @@ function registerHooks(api, cfg) {
|
|
|
12858
13077
|
).catch((err) => {
|
|
12859
13078
|
logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
|
|
12860
13079
|
});
|
|
13080
|
+
initCommandSubscription(cfg, logger);
|
|
12861
13081
|
const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
|
|
12862
13082
|
for (const agentId of allAgentIds) {
|
|
12863
13083
|
const agentName = resolveAgentName(agentId);
|
package/dist/package.json
CHANGED