assistme 0.2.8 → 0.2.9

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.
@@ -1,4 +1,4 @@
1
- import { getSupabase } from "../db/supabase.js";
1
+ import { callMcpHandler } from "../db/api-client.js";
2
2
  import { log } from "../utils/logger.js";
3
3
 
4
4
  export type MemoryCategory =
@@ -26,10 +26,8 @@ export interface Memory {
26
26
  // ── Memory Manager ──────────────────────────────────────────────────
27
27
 
28
28
  export class MemoryManager {
29
- private userId: string;
30
-
31
- constructor(userId: string) {
32
- this.userId = userId;
29
+ constructor(_userId: string) {
30
+ // userId is no longer needed — auth is handled by the MCP token in callMcpHandler
33
31
  }
34
32
 
35
33
  /**
@@ -46,74 +44,38 @@ export class MemoryManager {
46
44
  expiresInDays?: number;
47
45
  }
48
46
  ): Promise<Memory> {
49
- const sb = getSupabase();
50
-
51
47
  const expiresAt = options?.expiresInDays
52
48
  ? new Date(
53
49
  Date.now() + options.expiresInDays * 86400_000
54
50
  ).toISOString()
55
51
  : null;
56
52
 
57
- const { data, error } = await sb
58
- .from("agent_memories")
59
- .insert({
60
- user_id: this.userId,
61
- category,
62
- content,
63
- importance: options?.importance ?? 5,
64
- tags: options?.tags ?? [],
65
- source_message_id: options?.sourceMessageId ?? null,
66
- expires_at: expiresAt,
67
- })
68
- .select()
69
- .single();
53
+ const data = await callMcpHandler<Memory>("memory.store", {
54
+ category,
55
+ content,
56
+ importance: options?.importance ?? 5,
57
+ tags: options?.tags ?? [],
58
+ source_message_id: options?.sourceMessageId ?? null,
59
+ expires_at: expiresAt,
60
+ });
70
61
 
71
- if (error) throw new Error(`Failed to store memory: ${error.message}`);
72
62
  log.debug(`Memory stored: [${category}] ${content.slice(0, 80)}...`);
73
- return data as Memory;
63
+ return data;
74
64
  }
75
65
 
76
66
  /**
77
67
  * Search memories by query text. Uses ILIKE + tag containment.
78
68
  */
79
69
  async search(query: string, limit = 10): Promise<Memory[]> {
80
- const sb = getSupabase();
81
-
82
- // Sanitize query for use in ILIKE to prevent injection
83
- const sanitized = query.replace(/[%_]/g, "\\$&");
84
-
85
- const { data, error } = await sb
86
- .from("agent_memories")
87
- .select("*")
88
- .eq("user_id", this.userId)
89
- .or(
90
- `content.ilike.%${sanitized}%,tags.cs.{${sanitized}}`
91
- )
92
- .order("importance", { ascending: false })
93
- .limit(limit);
94
-
95
- if (error) {
96
- log.warn(`Memory search failed: ${error.message}`);
70
+ try {
71
+ return await callMcpHandler<Memory[]>("memory.search", {
72
+ query,
73
+ limit,
74
+ });
75
+ } catch (err) {
76
+ log.warn(`Memory search failed: ${err instanceof Error ? err.message : err}`);
97
77
  return [];
98
78
  }
99
-
100
- // Increment access_count for each matched memory individually
101
- if (data && data.length > 0) {
102
- const now = new Date().toISOString();
103
- await Promise.all(
104
- data.map((m) =>
105
- sb
106
- .from("agent_memories")
107
- .update({
108
- access_count: m.access_count + 1,
109
- last_accessed_at: now,
110
- })
111
- .eq("id", m.id)
112
- )
113
- );
114
- }
115
-
116
- return (data || []) as Memory[];
117
79
  }
118
80
 
119
81
  /**
@@ -122,49 +84,13 @@ export class MemoryManager {
122
84
  * Automatically filters out expired memories.
123
85
  */
124
86
  async getContext(maxItems = 20): Promise<Memory[]> {
125
- const sb = getSupabase();
126
- const now = new Date().toISOString();
127
-
128
- // Get instructions first (always relevant)
129
- const { data: instructions } = await sb
130
- .from("agent_memories")
131
- .select("*")
132
- .eq("user_id", this.userId)
133
- .eq("category", "instruction")
134
- .or(`expires_at.is.null,expires_at.gt.${now}`)
135
- .order("importance", { ascending: false })
136
- .limit(5);
137
-
138
- // Get preferences
139
- const { data: preferences } = await sb
140
- .from("agent_memories")
141
- .select("*")
142
- .eq("user_id", this.userId)
143
- .eq("category", "preference")
144
- .or(`expires_at.is.null,expires_at.gt.${now}`)
145
- .order("importance", { ascending: false })
146
- .limit(5);
147
-
148
- // Get most important general memories
149
- const { data: general } = await sb
150
- .from("agent_memories")
151
- .select("*")
152
- .eq("user_id", this.userId)
153
- .not("category", "in", '("instruction","preference")')
154
- .or(`expires_at.is.null,expires_at.gt.${now}`)
155
- .order("importance", { ascending: false })
156
- .order("updated_at", { ascending: false })
157
- .limit(maxItems - 10);
158
-
159
- const all = [
160
- ...(instructions || []),
161
- ...(preferences || []),
162
- ...(general || []),
163
- ] as Memory[];
87
+ const all = await callMcpHandler<Memory[]>("memory.get_context", {
88
+ max_items: maxItems,
89
+ });
164
90
 
165
91
  // Deduplicate by id
166
92
  const seen = new Set<string>();
167
- return all.filter((m) => {
93
+ return (all || []).filter((m) => {
168
94
  if (seen.has(m.id)) return false;
169
95
  seen.add(m.id);
170
96
  return true;
@@ -210,22 +136,11 @@ export class MemoryManager {
210
136
  category?: MemoryCategory,
211
137
  limit = 20
212
138
  ): Promise<Memory[]> {
213
- const sb = getSupabase();
214
- let query = sb
215
- .from("agent_memories")
216
- .select("*")
217
- .eq("user_id", this.userId)
218
- .order("importance", { ascending: false })
219
- .order("created_at", { ascending: false })
220
- .limit(limit);
221
-
222
- if (category) {
223
- query = query.eq("category", category);
224
- }
225
-
226
- const { data, error } = await query;
227
- if (error) throw new Error(`Failed to list memories: ${error.message}`);
228
- return (data || []) as Memory[];
139
+ const data = await callMcpHandler<Memory[]>("memory.list", {
140
+ category: category || null,
141
+ limit,
142
+ });
143
+ return data || [];
229
144
  }
230
145
 
231
146
  async add(
@@ -238,29 +153,13 @@ export class MemoryManager {
238
153
  }
239
154
 
240
155
  async remove(memoryId: string): Promise<void> {
241
- const sb = getSupabase();
242
- const { error } = await sb
243
- .from("agent_memories")
244
- .delete()
245
- .eq("id", memoryId)
246
- .eq("user_id", this.userId);
247
-
248
- if (error) throw new Error(`Failed to delete memory: ${error.message}`);
156
+ await callMcpHandler("memory.remove", { memory_id: memoryId });
249
157
  }
250
158
 
251
159
  async clear(category?: MemoryCategory): Promise<number> {
252
- const sb = getSupabase();
253
- let query = sb
254
- .from("agent_memories")
255
- .delete()
256
- .eq("user_id", this.userId);
257
-
258
- if (category) {
259
- query = query.eq("category", category);
260
- }
261
-
262
- const { error, count } = await query.select("id");
263
- if (error) throw new Error(`Failed to clear memories: ${error.message}`);
264
- return count || 0;
160
+ const result = await callMcpHandler<{ count: number }>("memory.clear", {
161
+ category: category || null,
162
+ });
163
+ return result.count;
265
164
  }
266
165
  }
@@ -65,8 +65,6 @@ Available capabilities:
65
65
  - If the user approves, use skill_add to add it to their collection, then proceed with the task
66
66
  - If a skill's instructions could be improved based on your experience, use skill_improve
67
67
  - Use skill_search to find relevant skills when the task doesn't obviously match the listed skills
68
- - Skills use {{variable_name}} placeholders for user-specific data (repos, channels, boards, etc.)
69
- - Use skill_configure to set variable values after creating skills or when the user provides their data
70
68
 
71
69
  5. JOB AUTOMATION:
72
70
  - When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
@@ -234,7 +232,6 @@ export class TaskProcessor {
234
232
  "mcp__assistme-agent__skill_improve",
235
233
  "mcp__assistme-agent__skill_invoke",
236
234
  "mcp__assistme-agent__skill_search",
237
- "mcp__assistme-agent__skill_configure",
238
235
  "mcp__assistme-agent__skill_generate",
239
236
  "mcp__assistme-agent__skill_link_job",
240
237
  "mcp__assistme-agent__skill_browse",
@@ -1,4 +1,4 @@
1
- import { getSupabase, getCurrentUserId } from "../db/supabase.js";
1
+ import { callMcpHandler } from "../db/api-client.js";
2
2
  import { log } from "../utils/logger.js";
3
3
 
4
4
  const SCHEDULER_INTERVAL = 30_000; // Check every 30 seconds
@@ -35,10 +35,8 @@ export function getNextRunTime(cronExpr: string, timezone: string, fromDate?: Da
35
35
 
36
36
  const [minExpr, hourExpr, domExpr, monExpr, dowExpr] = parts;
37
37
 
38
- // Simple cron parser — handles: *, */N, N, N-M, N,M
39
38
  function parseField(expr: string, min: number, max: number): number[] {
40
39
  const values: number[] = [];
41
-
42
40
  for (const part of expr.split(",")) {
43
41
  if (part === "*") {
44
42
  for (let i = min; i <= max; i++) values.push(i);
@@ -52,7 +50,6 @@ export function getNextRunTime(cronExpr: string, timezone: string, fromDate?: Da
52
50
  values.push(parseInt(part));
53
51
  }
54
52
  }
55
-
56
53
  return values.sort((a, b) => a - b);
57
54
  }
58
55
 
@@ -60,17 +57,14 @@ export function getNextRunTime(cronExpr: string, timezone: string, fromDate?: Da
60
57
  const hours = parseField(hourExpr, 0, 23);
61
58
  const daysOfMonth = parseField(domExpr, 1, 31);
62
59
  const months = parseField(monExpr, 1, 12);
63
- const daysOfWeek = parseField(dowExpr, 0, 6); // 0 = Sunday
60
+ const daysOfWeek = parseField(dowExpr, 0, 6);
64
61
 
65
62
  const useUTC = timezone === "UTC";
66
63
 
67
- // Find the next matching time after 'now'
68
- const candidate = new Date(now.getTime() + 60_000); // Start from next minute
64
+ const candidate = new Date(now.getTime() + 60_000);
69
65
  candidate.setSeconds(0, 0);
70
66
 
71
- // Search up to 366 days ahead
72
67
  for (let i = 0; i < 527040; i++) {
73
- // 366 * 24 * 60
74
68
  const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
75
69
  const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
76
70
  const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
@@ -87,10 +81,9 @@ export function getNextRunTime(cronExpr: string, timezone: string, fromDate?: Da
87
81
  return candidate;
88
82
  }
89
83
 
90
- candidate.setTime(candidate.getTime() + 60_000); // Advance 1 minute
84
+ candidate.setTime(candidate.getTime() + 60_000);
91
85
  }
92
86
 
93
- // Fallback: 24 hours from now
94
87
  return new Date(now.getTime() + 86400_000);
95
88
  }
96
89
 
@@ -103,10 +96,8 @@ export class Scheduler {
103
96
  this.onScheduledTask = onScheduledTask;
104
97
  this.running = true;
105
98
 
106
- // Initialize next_run_at for tasks that don't have it yet
107
99
  await this.initializeNextRuns();
108
100
 
109
- // Check for due tasks periodically
110
101
  this.timer = setInterval(() => this.checkDueTasks(), SCHEDULER_INTERVAL);
111
102
  log.info("Scheduler started (checking every 30s)");
112
103
  }
@@ -121,22 +112,15 @@ export class Scheduler {
121
112
 
122
113
  private async initializeNextRuns(): Promise<void> {
123
114
  try {
124
- const userId = await getCurrentUserId();
125
- const sb = getSupabase();
126
- const { data } = await sb
127
- .from("agent_scheduled_tasks")
128
- .select("*")
129
- .eq("user_id", userId)
130
- .eq("enabled", true)
131
- .is("next_run_at", null);
115
+ const data = await callMcpHandler<ScheduledTask[]>("schedule.get_uninitialized");
132
116
 
133
117
  if (data) {
134
118
  for (const task of data) {
135
119
  const nextRun = getNextRunTime(task.cron_expression, task.timezone);
136
- await sb
137
- .from("agent_scheduled_tasks")
138
- .update({ next_run_at: nextRun.toISOString() })
139
- .eq("id", task.id);
120
+ await callMcpHandler("schedule.update", {
121
+ task_id: task.id,
122
+ next_run_at: nextRun.toISOString(),
123
+ });
140
124
  }
141
125
  }
142
126
  } catch (err) {
@@ -148,44 +132,37 @@ export class Scheduler {
148
132
  if (!this.running || !this.onScheduledTask) return;
149
133
 
150
134
  try {
151
- const userId = await getCurrentUserId();
152
- const sb = getSupabase();
153
-
154
- const { data: dueTasks } = await sb
155
- .from("agent_scheduled_tasks")
156
- .select("*")
157
- .eq("user_id", userId)
158
- .eq("enabled", true)
159
- .lte("next_run_at", new Date().toISOString())
160
- .order("next_run_at", { ascending: true })
161
- .limit(1);
135
+ const dueTasks = await callMcpHandler<ScheduledTask[]>("schedule.check_due");
162
136
 
163
137
  if (!dueTasks || dueTasks.length === 0) return;
164
138
 
165
- const task = dueTasks[0] as ScheduledTask;
139
+ const task = dueTasks[0];
166
140
  log.info(`Scheduled task due: "${task.name}"`);
167
141
 
168
- // Calculate next run before executing
169
142
  const nextRun = getNextRunTime(task.cron_expression, task.timezone);
170
143
 
171
144
  // Update: set running, advance next_run_at
172
- await sb
173
- .from("agent_scheduled_tasks")
174
- .update({
175
- last_run_at: new Date().toISOString(),
176
- next_run_at: nextRun.toISOString(),
177
- run_count: task.run_count + 1,
178
- })
179
- .eq("id", task.id);
145
+ await callMcpHandler("schedule.update", {
146
+ task_id: task.id,
147
+ last_run_at: new Date().toISOString(),
148
+ next_run_at: nextRun.toISOString(),
149
+ run_count: task.run_count + 1,
150
+ });
180
151
 
181
152
  // Execute
182
153
  try {
183
154
  await this.onScheduledTask(task);
184
155
 
185
- await sb.from("agent_scheduled_tasks").update({ last_error: null }).eq("id", task.id);
156
+ await callMcpHandler("schedule.update", {
157
+ task_id: task.id,
158
+ last_error: null,
159
+ });
186
160
  } catch (err) {
187
161
  const errMsg = err instanceof Error ? err.message : String(err);
188
- await sb.from("agent_scheduled_tasks").update({ last_error: errMsg }).eq("id", task.id);
162
+ await callMcpHandler("schedule.update", {
163
+ task_id: task.id,
164
+ last_error: errMsg,
165
+ });
189
166
  log.error(`Scheduled task "${task.name}" failed: ${errMsg}`);
190
167
  }
191
168
  } catch (err) {
@@ -197,68 +174,45 @@ export class Scheduler {
197
174
  // ── CRUD helpers for CLI commands ──────────────────────────────────
198
175
 
199
176
  export async function createScheduledTask(
200
- userId: string,
177
+ _userId: string,
201
178
  name: string,
202
179
  prompt: string,
203
180
  cronExpression: string,
204
181
  timezone = "UTC"
205
182
  ): Promise<ScheduledTask> {
206
- const sb = getSupabase();
207
183
  const nextRun = getNextRunTime(cronExpression, timezone);
208
184
 
209
- const { data, error } = await sb
210
- .from("agent_scheduled_tasks")
211
- .insert({
212
- user_id: userId,
213
- name,
214
- prompt,
215
- cron_expression: cronExpression,
216
- timezone,
217
- next_run_at: nextRun.toISOString(),
218
- })
219
- .select()
220
- .single();
221
-
222
- if (error) throw new Error(`Failed to create schedule: ${error.message}`);
223
- return data as ScheduledTask;
185
+ return callMcpHandler<ScheduledTask>("schedule.create", {
186
+ name,
187
+ prompt,
188
+ cron_expression: cronExpression,
189
+ timezone,
190
+ next_run_at: nextRun.toISOString(),
191
+ });
224
192
  }
225
193
 
226
- export async function listScheduledTasks(userId: string): Promise<ScheduledTask[]> {
227
- const sb = getSupabase();
228
- const { data, error } = await sb
229
- .from("agent_scheduled_tasks")
230
- .select("*")
231
- .eq("user_id", userId)
232
- .order("created_at", { ascending: false });
233
-
234
- if (error) throw new Error(`Failed to list schedules: ${error.message}`);
235
- return (data || []) as ScheduledTask[];
194
+ export async function listScheduledTasks(_userId: string): Promise<ScheduledTask[]> {
195
+ return callMcpHandler<ScheduledTask[]>("schedule.list");
236
196
  }
237
197
 
238
198
  export async function toggleScheduledTask(taskId: string, enabled: boolean): Promise<void> {
239
- const sb = getSupabase();
240
- const update: Record<string, unknown> = { enabled };
199
+ const params: Record<string, unknown> = { task_id: taskId, enabled };
200
+
241
201
  if (enabled) {
242
- // Recalculate next run when re-enabling
243
- const { data } = await sb
244
- .from("agent_scheduled_tasks")
245
- .select("cron_expression, timezone")
246
- .eq("id", taskId)
247
- .single();
248
- if (data) {
249
- const nextRun = getNextRunTime(data.cron_expression, data.timezone);
250
- update.next_run_at = nextRun.toISOString();
202
+ // Need to recalculate next run when re-enabling
203
+ const taskData = await callMcpHandler<{ cron_expression: string; timezone: string }>(
204
+ "schedule.get_task",
205
+ { task_id: taskId },
206
+ );
207
+ if (taskData) {
208
+ const nextRun = getNextRunTime(taskData.cron_expression, taskData.timezone);
209
+ params.next_run_at = nextRun.toISOString();
251
210
  }
252
211
  }
253
212
 
254
- const { error } = await sb.from("agent_scheduled_tasks").update(update).eq("id", taskId);
255
-
256
- if (error) throw new Error(`Failed to toggle schedule: ${error.message}`);
213
+ await callMcpHandler("schedule.toggle", params);
257
214
  }
258
215
 
259
216
  export async function deleteScheduledTask(taskId: string): Promise<void> {
260
- const sb = getSupabase();
261
- const { error } = await sb.from("agent_scheduled_tasks").delete().eq("id", taskId);
262
-
263
- if (error) throw new Error(`Failed to delete schedule: ${error.message}`);
217
+ await callMcpHandler("schedule.delete", { task_id: taskId });
264
218
  }
@@ -15,17 +15,6 @@ const mockSession = {
15
15
  metadata: {},
16
16
  };
17
17
 
18
- // Build a fluent Supabase chain that does nothing
19
- const chain: Record<string, unknown> = {};
20
- const methods = [
21
- "select", "insert", "update", "delete", "eq", "neq", "not",
22
- "or", "in", "order", "limit", "single", "from", "lt",
23
- ];
24
- for (const method of methods) {
25
- chain[method] = vi.fn().mockReturnValue(chain);
26
- }
27
- chain.then = (resolve: (value: unknown) => void) => resolve({ data: [], error: null });
28
-
29
18
  const mockCreateSession = vi.fn().mockResolvedValue(mockSession);
30
19
  const mockUpdateHeartbeat = vi.fn().mockResolvedValue(undefined);
31
20
  const mockEndSession = vi.fn().mockResolvedValue(undefined);
@@ -35,6 +24,8 @@ const mockClaimTask = vi.fn().mockResolvedValue(true);
35
24
  const mockCreateTask = vi.fn().mockResolvedValue({ id: "task-001", prompt: "test" });
36
25
  const mockGetOrCreateCliConversation = vi.fn().mockResolvedValue("conv-001");
37
26
 
27
+ const mockCleanupStaleSessions = vi.fn().mockResolvedValue(0);
28
+
38
29
  vi.mock("../db/supabase.js", () => ({
39
30
  createSession: (...args: unknown[]) => mockCreateSession(...args),
40
31
  updateHeartbeat: (...args: unknown[]) => mockUpdateHeartbeat(...args),
@@ -44,7 +35,12 @@ vi.mock("../db/supabase.js", () => ({
44
35
  claimTask: (...args: unknown[]) => mockClaimTask(...args),
45
36
  createTask: (...args: unknown[]) => mockCreateTask(...args),
46
37
  getOrCreateCliConversation: (...args: unknown[]) => mockGetOrCreateCliConversation(...args),
47
- getSupabase: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue(chain) }),
38
+ cleanupStaleSessions: (...args: unknown[]) => mockCleanupStaleSessions(...args),
39
+ pollAndClaimJobRun: vi.fn().mockResolvedValue(null),
40
+ }));
41
+
42
+ vi.mock("../db/api-client.js", () => ({
43
+ callMcpHandler: vi.fn().mockResolvedValue(null),
48
44
  }));
49
45
 
50
46
  vi.mock("../utils/config.js", () => ({
@@ -8,11 +8,12 @@ import {
8
8
  claimTask,
9
9
  createTask,
10
10
  getOrCreateCliConversation,
11
- getSupabase,
11
+ cleanupStaleSessions,
12
12
  AgentSession,
13
13
  ConversationMessage,
14
14
  PendingJobRun,
15
15
  } from "../db/supabase.js";
16
+ import { callMcpHandler } from "../db/api-client.js";
16
17
  import { getConfig } from "../utils/config.js";
17
18
  import { log } from "../utils/logger.js";
18
19
  import { Scheduler, ScheduledTask } from "./scheduler.js";
@@ -21,7 +22,6 @@ import { JobRunner } from "./job-runner.js";
21
22
  const DEFAULT_HEARTBEAT_INTERVAL = 30_000; // 30 seconds
22
23
  const DEFAULT_POLL_INTERVAL = 2_000; // 2 seconds
23
24
  const MAX_POLL_INTERVAL = 30_000; // Max backoff: 30 seconds
24
- const STALE_SESSION_THRESHOLD = 120_000; // 2 minutes without heartbeat = stale
25
25
 
26
26
  export class SessionManager {
27
27
  private session: AgentSession | null = null;
@@ -93,9 +93,6 @@ export class SessionManager {
93
93
  return this.session;
94
94
  }
95
95
 
96
- /**
97
- * Schedule the next poll with exponential backoff on failures.
98
- */
99
96
  private schedulePoll(): void {
100
97
  if (!this.running) return;
101
98
 
@@ -126,10 +123,6 @@ export class SessionManager {
126
123
  }
127
124
  }
128
125
 
129
- /**
130
- * Execute a pending job run triggered from the web UI.
131
- * Loads the job, builds the agentic prompt, and processes it as a chat task.
132
- */
133
126
  private async executeJobRun(jobRun: PendingJobRun): Promise<void> {
134
127
  if (!this.session || !this.userId || !this.conversationId || !this.onTask)
135
128
  return;
@@ -157,11 +150,10 @@ export class SessionManager {
157
150
 
158
151
  // Link session to run record (non-critical)
159
152
  try {
160
- const sb = getSupabase();
161
- await sb
162
- .from("agent_job_runs")
163
- .update({ session_id: this.session.id })
164
- .eq("id", jobRun.id);
153
+ await callMcpHandler("job.link_run_session", {
154
+ run_id: jobRun.id,
155
+ session_id: this.session.id,
156
+ });
165
157
  } catch (linkErr) {
166
158
  log.debug(`Failed to link session to job run: ${linkErr}`);
167
159
  }
@@ -253,50 +245,19 @@ export class SessionManager {
253
245
  this.schedulePoll();
254
246
  }
255
247
 
256
- /**
257
- * Mark sessions as offline if they haven't sent a heartbeat recently.
258
- * Runs once on startup to clean up orphaned sessions from crashed processes.
259
- */
260
248
  private async cleanupStaleSessions(): Promise<void> {
261
- if (!this.userId) return;
249
+ if (!this.userId || !this.session) return;
262
250
 
263
251
  try {
264
- const sb = getSupabase();
265
- const threshold = new Date(
266
- Date.now() - STALE_SESSION_THRESHOLD
267
- ).toISOString();
268
-
269
- const { data: stale } = await sb
270
- .from("agent_sessions")
271
- .select("id")
272
- .eq("user_id", this.userId)
273
- .in("status", ["online", "busy"])
274
- .lt("last_heartbeat_at", threshold)
275
- .neq("id", this.session?.id || "");
276
-
277
- if (stale && stale.length > 0) {
278
- for (const s of stale) {
279
- await sb
280
- .from("agent_sessions")
281
- .update({
282
- status: "offline",
283
- ended_at: new Date().toISOString(),
284
- metadata: { ended_reason: "stale_session_cleanup" },
285
- })
286
- .eq("id", s.id);
287
- }
288
- log.info(`Cleaned up ${stale.length} stale session(s)`);
252
+ const cleaned = await cleanupStaleSessions(this.session.id);
253
+ if (cleaned > 0) {
254
+ log.info(`Cleaned up ${cleaned} stale session(s)`);
289
255
  }
290
256
  } catch (err) {
291
257
  log.debug(`Stale session cleanup error: ${err}`);
292
258
  }
293
259
  }
294
260
 
295
- /**
296
- * Submit a task from the interactive prompt or scheduled job.
297
- * Sets processing=true BEFORE creating the task so the poll loop
298
- * never races to pick it up.
299
- */
300
261
  async submitTask(prompt: string): Promise<void> {
301
262
  if (!this.session || !this.userId || !this.conversationId || !this.onTask) {
302
263
  throw new Error("Session not started");
@@ -328,9 +289,6 @@ export class SessionManager {
328
289
  }
329
290
  }
330
291
 
331
- /**
332
- * Stop the session with a safety timeout to prevent hanging on shutdown.
333
- */
334
292
  async stop(timeoutMs = 5_000): Promise<void> {
335
293
  this.running = false;
336
294
  this.scheduler.stop();
@@ -347,7 +305,6 @@ export class SessionManager {
347
305
 
348
306
  if (this.session) {
349
307
  try {
350
- // Wrap DB call with a deadline to avoid hanging on shutdown
351
308
  await Promise.race([
352
309
  endSession(this.session.id),
353
310
  new Promise<never>((_, reject) =>