openclaw-remote 0.3.1 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +119 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2795,7 +2795,67 @@ async function getTeam(account) {
2795
2795
 
2796
2796
  // src/realtime-listener.ts
2797
2797
  import { createClient } from "@supabase/supabase-js";
2798
- function connectRealtime(account, onEvent, onError, log) {
2798
+ var leaderTimers = /* @__PURE__ */ new Map();
2799
+ var LEADER_DEBOUNCE_MS = 6e4;
2800
+ var leaderRolesCache = [];
2801
+ var leaderRolesCacheTime = 0;
2802
+ var LEADER_CACHE_TTL = 5 * 6e4;
2803
+ async function resolveAgentIdentity(account, log) {
2804
+ try {
2805
+ const res = await fetch(`${account.baseUrl}/api/v1/roles`, {
2806
+ headers: { Authorization: `Bearer ${account.apiKey}`, Accept: "application/json" },
2807
+ signal: AbortSignal.timeout(1e4)
2808
+ });
2809
+ if (!res.ok) {
2810
+ log?.warn?.(`Failed to resolve agent identity: HTTP ${res.status}`);
2811
+ return null;
2812
+ }
2813
+ const data = await res.json();
2814
+ const roles = data.roles || [];
2815
+ const myRoles = roles.filter((r) => r.is_mine);
2816
+ const myRoleIds = myRoles.map((r) => r.id);
2817
+ const isLeader = myRoles.some((r) => r.is_leader);
2818
+ const hbRes = await fetch(`${account.baseUrl}/api/v1/heartbeat`, {
2819
+ headers: { Authorization: `Bearer ${account.apiKey}`, Accept: "application/json" },
2820
+ signal: AbortSignal.timeout(1e4)
2821
+ });
2822
+ let agentName = "";
2823
+ if (hbRes.ok) {
2824
+ const hb = await hbRes.json();
2825
+ agentName = hb.agent_name || "";
2826
+ }
2827
+ log?.info?.(`Agent identity resolved: "${agentName}", roles: [${myRoleIds.join(", ")}], leader: ${isLeader}`);
2828
+ return { agentName, myRoleIds, isLeader };
2829
+ } catch (err) {
2830
+ log?.warn?.(`Failed to resolve agent identity: ${err}`);
2831
+ return null;
2832
+ }
2833
+ }
2834
+ async function fetchLeaderRoles(supabase, log) {
2835
+ const now = Date.now();
2836
+ if (now - leaderRolesCacheTime < LEADER_CACHE_TTL && leaderRolesCache.length > 0) {
2837
+ return leaderRolesCache;
2838
+ }
2839
+ const { data: roles } = await supabase.from("project_roles").select("id").eq("is_leader", true);
2840
+ if (!roles || roles.length === 0) {
2841
+ leaderRolesCache = [];
2842
+ leaderRolesCacheTime = now;
2843
+ return [];
2844
+ }
2845
+ const roleIds = roles.map((r) => r.id);
2846
+ const { data: assignments } = await supabase.from("agent_role_assignments").select("project_role_id, agent_id").in("project_role_id", roleIds);
2847
+ const map = /* @__PURE__ */ new Map();
2848
+ for (const a of assignments ?? []) {
2849
+ const list = map.get(a.project_role_id) || [];
2850
+ list.push(a.agent_id);
2851
+ map.set(a.project_role_id, list);
2852
+ }
2853
+ leaderRolesCache = roleIds.map((id) => ({ roleId: id, agentIds: map.get(id) || [] }));
2854
+ leaderRolesCacheTime = now;
2855
+ log?.info?.(`Leader roles cache refreshed: ${leaderRolesCache.length} leader role(s)`);
2856
+ return leaderRolesCache;
2857
+ }
2858
+ function connectRealtime(account, identity, onEvent, onError, log) {
2799
2859
  const { supabaseUrl, supabaseKey } = account;
2800
2860
  if (!supabaseUrl || !supabaseKey) {
2801
2861
  throw new Error("Supabase URL and key are required for Realtime listener. Set supabaseUrl and supabaseKey in channels.remote config.");
@@ -2819,14 +2879,31 @@ function connectRealtime(account, onEvent, onError, log) {
2819
2879
  const row = payload.new;
2820
2880
  if (row.agent_author_id && !row.author_id) {
2821
2881
  log?.info?.(`Skipping agent-authored comment on task ${row.task_id} (echo suppression)`);
2882
+ scheduleLeaderNotification(row.task_id, row.agent_author_id, supabase, log);
2822
2883
  return;
2823
2884
  }
2885
+ const body = row.body || row.content || "";
2886
+ if (!row.author_id && !row.agent_author_id && identity) {
2887
+ if (body.startsWith("\u{1F4CC} Assigned to ")) {
2888
+ const targetName = body.replace("\u{1F4CC} Assigned to ", "").trim();
2889
+ if (targetName.toLowerCase() !== identity.agentName.toLowerCase()) {
2890
+ log?.info?.(`Skipping assignment notification for ${targetName} (I am ${identity.agentName})`);
2891
+ return;
2892
+ }
2893
+ }
2894
+ if (body.startsWith("\u{1F440} ")) {
2895
+ if (!identity.isLeader) {
2896
+ log?.info?.(`Skipping leader notification (I am not a leader)`);
2897
+ return;
2898
+ }
2899
+ }
2900
+ }
2824
2901
  const event = {
2825
2902
  type: "task.commented",
2826
2903
  task_id: row.task_id,
2827
2904
  comment: {
2828
2905
  id: row.id,
2829
- content: row.body || row.content || "",
2906
+ content: body,
2830
2907
  author_name: row.author_name || "System",
2831
2908
  author_id: row.author_id || "",
2832
2909
  created_at: row.created_at
@@ -2854,11 +2931,49 @@ function connectRealtime(account, onEvent, onError, log) {
2854
2931
  return {
2855
2932
  unsubscribe: async () => {
2856
2933
  log?.info?.("Unsubscribing from Supabase Realtime...");
2934
+ for (const timer of leaderTimers.values()) clearTimeout(timer);
2935
+ leaderTimers.clear();
2857
2936
  await supabase.removeChannel(channel);
2858
2937
  log?.info?.("Realtime listener shut down cleanly.");
2859
2938
  }
2860
2939
  };
2861
2940
  }
2941
+ async function scheduleLeaderNotification(taskId, commentingAgentId, supabase, log) {
2942
+ const existing = leaderTimers.get(taskId);
2943
+ if (existing) clearTimeout(existing);
2944
+ const timer = setTimeout(async () => {
2945
+ leaderTimers.delete(taskId);
2946
+ try {
2947
+ const leaders = await fetchLeaderRoles(supabase, log);
2948
+ if (leaders.length === 0) return;
2949
+ const { data: task } = await supabase.from("tasks").select("id, title, assigned_role_id").eq("id", taskId).single();
2950
+ if (!task) return;
2951
+ for (const leader of leaders) {
2952
+ if (task.assigned_role_id === leader.roleId) {
2953
+ log?.info?.(`Skipping leader notification for task ${taskId} \u2014 assigned to leader role ${leader.roleId}`);
2954
+ continue;
2955
+ }
2956
+ if (leader.agentIds.includes(commentingAgentId)) continue;
2957
+ if (leader.agentIds.length === 0) continue;
2958
+ const { error } = await supabase.from("task_comments").insert({
2959
+ task_id: taskId,
2960
+ author_id: null,
2961
+ agent_author_id: null,
2962
+ body: `\u{1F440} Activity on this task \u2014 leader review needed`
2963
+ });
2964
+ if (error) {
2965
+ log?.warn?.(`Failed to insert leader notification for task ${taskId}: ${error.message}`);
2966
+ } else {
2967
+ log?.info?.(`Leader notification sent for task ${taskId}`);
2968
+ }
2969
+ }
2970
+ } catch (err) {
2971
+ log?.warn?.(`Leader notification error for task ${taskId}: ${err}`);
2972
+ }
2973
+ }, LEADER_DEBOUNCE_MS);
2974
+ leaderTimers.set(taskId, timer);
2975
+ log?.info?.(`Leader debounce timer set for task ${taskId} (60s)`);
2976
+ }
2862
2977
 
2863
2978
  // src/event-formatter.ts
2864
2979
  function formatEvent(event) {
@@ -3163,8 +3278,10 @@ function createRemotePlugin() {
3163
3278
  let realtimeHandle = null;
3164
3279
  if (account.supabaseUrl && account.supabaseKey) {
3165
3280
  try {
3281
+ const identity = await resolveAgentIdentity(account, log);
3166
3282
  realtimeHandle = connectRealtime(
3167
3283
  account,
3284
+ identity,
3168
3285
  async (event) => {
3169
3286
  const formatted = formatEvent(event);
3170
3287
  if (!formatted) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-remote",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Remote project board channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",