assistme 0.1.4 → 0.1.5

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.
@@ -138,22 +138,6 @@ function writeAuthStore(data) {
138
138
  ensureAuthDir();
139
139
  writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
140
140
  }
141
- var fileStorage = {
142
- getItem(key) {
143
- const store = readAuthStore();
144
- return store[key] ?? null;
145
- },
146
- setItem(key, value) {
147
- const store = readAuthStore();
148
- store[key] = value;
149
- writeAuthStore(store);
150
- },
151
- removeItem(key) {
152
- const store = readAuthStore();
153
- delete store[key];
154
- writeAuthStore(store);
155
- }
156
- };
157
141
  var supabase = null;
158
142
  function getSupabase() {
159
143
  if (!supabase) {
@@ -164,12 +148,7 @@ function getSupabase() {
164
148
  );
165
149
  }
166
150
  supabase = createClient(config2.supabaseUrl, config2.supabaseAnonKey, {
167
- auth: {
168
- storage: fileStorage,
169
- autoRefreshToken: false,
170
- // We handle refresh via am_ token
171
- persistSession: true
172
- }
151
+ auth: { persistSession: false }
173
152
  });
174
153
  }
175
154
  return supabase;
@@ -177,16 +156,13 @@ function getSupabase() {
177
156
  function hashToken(token) {
178
157
  return createHash("sha256").update(token).digest("hex");
179
158
  }
180
- async function authenticateWithMcpToken(mcpToken) {
181
- const tokenHash = hashToken(mcpToken);
182
- const sb = getSupabase();
183
- const { data, error } = await sb.rpc("login_with_mcp_token", {
184
- p_token_hash: tokenHash
185
- });
186
- if (error) throw new Error(`Token validation failed: ${error.message}`);
187
- if (data?.error) throw new Error(data.error);
188
- if (!data?.access_token) throw new Error("Token validation returned no session");
189
- return data;
159
+ function getTokenHash() {
160
+ const store = readAuthStore();
161
+ const token = store["mcp_token"];
162
+ if (!token || !token.startsWith("am_")) {
163
+ throw new Error("Not authenticated. Run `assistme login`.");
164
+ }
165
+ return hashToken(token);
190
166
  }
191
167
  async function loginWithToken(mcpToken) {
192
168
  if (!mcpToken.startsWith("am_")) {
@@ -194,195 +170,149 @@ async function loginWithToken(mcpToken) {
194
170
  "Invalid token format. Use an am_ token from the web page."
195
171
  );
196
172
  }
197
- const { access_token, user_id } = await authenticateWithMcpToken(mcpToken);
198
- const store = readAuthStore();
199
- store["mcp_token"] = mcpToken;
200
- writeAuthStore(store);
173
+ const hash = hashToken(mcpToken);
201
174
  const sb = getSupabase();
202
- const { data, error } = await sb.auth.setSession({
203
- access_token,
204
- refresh_token: "mcp_managed"
205
- // placeholder; we refresh via am_ token
175
+ const { data, error } = await sb.rpc("validate_mcp_token", {
176
+ p_token_hash: hash
206
177
  });
207
- if (error) throw new Error(`Login failed: ${error.message}`);
208
- if (!data.user) throw new Error("Login failed: no user returned");
209
- return data.user.id;
210
- }
211
- async function refreshWithCliToken() {
178
+ if (error) throw new Error(`Token validation failed: ${error.message}`);
179
+ if (!data || data.length === 0) throw new Error("Invalid or expired token");
212
180
  const store = readAuthStore();
213
- const mcpToken = store["mcp_token"];
214
- if (!mcpToken || !mcpToken.startsWith("am_")) return null;
215
- try {
216
- const { access_token } = await authenticateWithMcpToken(mcpToken);
217
- const sb = getSupabase();
218
- const { data, error } = await sb.auth.setSession({
219
- access_token,
220
- refresh_token: "mcp_managed"
221
- });
222
- if (error || !data.user) return null;
223
- return data.user.id;
224
- } catch {
225
- return null;
226
- }
227
- }
228
- async function getSession() {
229
- const sb = getSupabase();
230
- const { data } = await sb.auth.getSession();
231
- return data.session;
181
+ store["mcp_token"] = mcpToken;
182
+ writeAuthStore(store);
183
+ return data[0].out_user_id;
232
184
  }
233
185
  async function getCurrentUserId() {
186
+ const hash = getTokenHash();
234
187
  const sb = getSupabase();
235
- const { data, error } = await sb.auth.getUser();
236
- if (error || !data.user) {
237
- const refreshed = await refreshWithCliToken();
238
- if (refreshed) return refreshed;
239
- throw new Error("Not authenticated. Run `assistme login`.");
188
+ const { data, error } = await sb.rpc("validate_mcp_token", {
189
+ p_token_hash: hash
190
+ });
191
+ if (error || !data || data.length === 0) {
192
+ throw new Error("Token expired or revoked. Run `assistme login`.");
240
193
  }
241
- return data.user.id;
194
+ return data[0].out_user_id;
242
195
  }
243
196
  async function logout() {
244
- const sb = getSupabase();
245
- await sb.auth.signOut();
246
197
  try {
247
198
  writeAuthStore({});
248
199
  } catch {
249
200
  }
250
201
  }
251
- async function createSession(userId, sessionName, workspacePath, version) {
202
+ async function createSession(_userId, sessionName, workspacePath, version) {
252
203
  const sb = getSupabase();
253
- const { data, error } = await sb.from("agent_sessions").insert({
254
- user_id: userId,
255
- session_name: sessionName,
256
- status: "online",
257
- workspace_path: workspacePath,
258
- agent_version: version,
259
- metadata: { model: getConfig().model }
260
- }).select().single();
204
+ const { data, error } = await sb.rpc("mcp_create_session", {
205
+ p_token_hash: getTokenHash(),
206
+ p_session_name: sessionName,
207
+ p_workspace_path: workspacePath,
208
+ p_version: version,
209
+ p_model: getConfig().model || null
210
+ });
261
211
  if (error) throw new Error(`Failed to create session: ${error.message}`);
262
212
  return data;
263
213
  }
264
214
  async function updateHeartbeat(sessionId) {
265
215
  const sb = getSupabase();
266
- const { error } = await sb.from("agent_sessions").update({ last_heartbeat_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", sessionId);
216
+ const { error } = await sb.rpc("mcp_heartbeat", {
217
+ p_token_hash: getTokenHash(),
218
+ p_session_id: sessionId
219
+ });
267
220
  if (error) log.warn(`Heartbeat update failed: ${error.message}`);
268
221
  }
269
222
  async function endSession(sessionId) {
270
223
  const sb = getSupabase();
271
- const { error } = await sb.from("agent_sessions").update({
272
- status: "offline",
273
- ended_at: (/* @__PURE__ */ new Date()).toISOString()
274
- }).eq("id", sessionId);
224
+ const { error } = await sb.rpc("mcp_end_session", {
225
+ p_token_hash: getTokenHash(),
226
+ p_session_id: sessionId
227
+ });
275
228
  if (error) log.error(`Failed to end session: ${error.message}`);
276
229
  }
277
230
  async function setSessionBusy(sessionId, busy) {
278
231
  const sb = getSupabase();
279
- await sb.from("agent_sessions").update({ status: busy ? "busy" : "online" }).eq("id", sessionId);
232
+ await sb.rpc("mcp_set_session_busy", {
233
+ p_token_hash: getTokenHash(),
234
+ p_session_id: sessionId,
235
+ p_busy: busy
236
+ });
280
237
  }
281
- async function getOrCreateCliConversation(userId, sessionId) {
238
+ async function getOrCreateCliConversation(_userId, _sessionId) {
282
239
  const sb = getSupabase();
283
- const { data: existing } = await sb.from("conversations").select("id").eq("agent", "cli").eq("created_by", userId).order("created_at", { ascending: false }).limit(1);
284
- if (existing && existing.length > 0) {
285
- return existing[0].id;
286
- }
287
- const { data: conv, error: convErr } = await sb.from("conversations").insert({
288
- conversation_type: "direct",
289
- agent: "cli",
290
- created_by: userId
291
- }).select("id").single();
292
- if (convErr || !conv) {
293
- throw new Error(`Failed to create conversation: ${convErr?.message}`);
294
- }
295
- await sb.from("conversation_participants").insert([
296
- { conversation_id: conv.id, user_id: userId, role: "member" },
297
- { conversation_id: conv.id, user_id: CLI_AGENT_ID, role: "member" }
298
- ]);
299
- return conv.id;
240
+ const { data, error } = await sb.rpc("mcp_get_or_create_conversation", {
241
+ p_token_hash: getTokenHash()
242
+ });
243
+ if (error) throw new Error(`Failed to get conversation: ${error.message}`);
244
+ return data;
300
245
  }
301
- async function createTask(conversationId, userId, sessionId, prompt) {
246
+ async function createTask(conversationId, _userId, sessionId, prompt) {
302
247
  const sb = getSupabase();
303
- const { data: rpcResult, error: rpcError } = await sb.rpc(
304
- "create_task_pair",
305
- {
306
- p_conversation_id: conversationId,
307
- p_user_id: userId,
308
- p_agent_id: CLI_AGENT_ID,
309
- p_session_id: sessionId,
310
- p_prompt: prompt
311
- }
312
- );
313
- if (!rpcError && rpcResult) {
314
- return { ...rpcResult, prompt };
315
- }
316
- if (rpcError) {
317
- log.debug(`create_task_pair RPC unavailable, using fallback: ${rpcError.message}`);
318
- }
319
- const { error: userErr } = await sb.from("conversation_messages").insert({
320
- conversation_id: conversationId,
321
- sender_id: userId,
322
- role: "user",
323
- content: prompt
248
+ const { data, error } = await sb.rpc("mcp_create_task", {
249
+ p_token_hash: getTokenHash(),
250
+ p_conversation_id: conversationId,
251
+ p_session_id: sessionId,
252
+ p_prompt: prompt
324
253
  });
325
- if (userErr) throw new Error(`Failed to create user message: ${userErr.message}`);
326
- const { data, error } = await sb.from("conversation_messages").insert({
327
- conversation_id: conversationId,
328
- sender_id: CLI_AGENT_ID,
329
- role: "assistant",
330
- content: "",
331
- status: "pending",
332
- session_id: sessionId,
333
- metadata: { prompt }
334
- }).select().single();
335
254
  if (error) throw new Error(`Failed to create task: ${error.message}`);
336
255
  return { ...data, prompt };
337
256
  }
338
257
  async function pollPendingTasks(sessionId) {
339
258
  const sb = getSupabase();
340
- const { data, error } = await sb.from("conversation_messages").select("*").eq("session_id", sessionId).eq("status", "pending").order("created_at", { ascending: true }).limit(1);
259
+ const { data, error } = await sb.rpc("mcp_poll_tasks", {
260
+ p_token_hash: getTokenHash(),
261
+ p_session_id: sessionId
262
+ });
341
263
  if (error) {
342
264
  log.warn(`Task poll failed: ${error.message}`);
343
265
  return [];
344
266
  }
345
- return (data || []).map((row) => ({
267
+ const rows = data || [];
268
+ return rows.map((row) => ({
346
269
  ...row,
347
270
  prompt: row.metadata?.prompt || row.content || ""
348
271
  }));
349
272
  }
273
+ async function pollAndClaimTask(sessionId) {
274
+ const sb = getSupabase();
275
+ const { data, error } = await sb.rpc("mcp_poll_and_claim_task", {
276
+ p_token_hash: getTokenHash(),
277
+ p_session_id: sessionId
278
+ });
279
+ if (error) {
280
+ log.warn(`Poll-and-claim failed: ${error.message}`);
281
+ return null;
282
+ }
283
+ if (!data) return null;
284
+ const row = data;
285
+ return {
286
+ ...row,
287
+ prompt: row.metadata?.prompt || row.content || ""
288
+ };
289
+ }
350
290
  async function claimTask(messageId) {
351
291
  const sb = getSupabase();
352
- const { error } = await sb.from("conversation_messages").update({
353
- status: "running",
354
- metadata: { started_at: (/* @__PURE__ */ new Date()).toISOString() }
355
- }).eq("id", messageId).eq("status", "pending");
292
+ const { data, error } = await sb.rpc("mcp_claim_task", {
293
+ p_token_hash: getTokenHash(),
294
+ p_message_id: messageId
295
+ });
356
296
  if (error) throw new Error(`Failed to claim task: ${error.message}`);
297
+ return data;
357
298
  }
358
299
  async function completeTask(messageId, resultSummary, tokenUsage) {
359
300
  const sb = getSupabase();
360
- const { data: current } = await sb.from("conversation_messages").select("metadata").eq("id", messageId).single();
361
- const existingMeta = current?.metadata || {};
362
- const { error } = await sb.from("conversation_messages").update({
363
- content: resultSummary,
364
- status: "completed",
365
- metadata: {
366
- ...existingMeta,
367
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
368
- token_usage: tokenUsage || null
369
- }
370
- }).eq("id", messageId);
301
+ const { error } = await sb.rpc("mcp_complete_task", {
302
+ p_token_hash: getTokenHash(),
303
+ p_message_id: messageId,
304
+ p_result: resultSummary,
305
+ p_token_usage: tokenUsage || null
306
+ });
371
307
  if (error) throw new Error(`Failed to complete task: ${error.message}`);
372
308
  }
373
309
  async function failTask(messageId, errorMessage) {
374
310
  const sb = getSupabase();
375
- const { data: current } = await sb.from("conversation_messages").select("metadata").eq("id", messageId).single();
376
- const existingMeta = current?.metadata || {};
377
- const { error } = await sb.from("conversation_messages").update({
378
- status: "failed",
379
- content: errorMessage,
380
- metadata: {
381
- ...existingMeta,
382
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
383
- error_message: errorMessage
384
- }
385
- }).eq("id", messageId);
311
+ const { error } = await sb.rpc("mcp_fail_task", {
312
+ p_token_hash: getTokenHash(),
313
+ p_message_id: messageId,
314
+ p_error: errorMessage
315
+ });
386
316
  if (error) log.error(`Failed to update task status: ${error.message}`);
387
317
  }
388
318
  var eventSequence = 0;
@@ -392,26 +322,26 @@ function resetEventSequence() {
392
322
  async function emitEvent(messageId, eventType, eventData) {
393
323
  const sb = getSupabase();
394
324
  eventSequence++;
395
- const { error } = await sb.from("message_events").insert({
396
- message_id: messageId,
397
- event_type: eventType,
398
- event_data: eventData,
399
- sequence_number: eventSequence
325
+ const { error } = await sb.rpc("mcp_emit_event", {
326
+ p_token_hash: getTokenHash(),
327
+ p_message_id: messageId,
328
+ p_event_type: eventType,
329
+ p_event_data: eventData,
330
+ p_seq: eventSequence
400
331
  });
401
332
  if (error) log.warn(`Failed to emit event: ${error.message}`);
402
333
  }
403
334
  async function emitEvents(messageId, events) {
404
335
  const sb = getSupabase();
405
- const rows = events.map((e) => {
336
+ const eventsJson = events.map((e) => {
406
337
  eventSequence++;
407
- return {
408
- message_id: messageId,
409
- event_type: e.type,
410
- event_data: e.data,
411
- sequence_number: eventSequence
412
- };
338
+ return { type: e.type, data: e.data, seq: eventSequence };
339
+ });
340
+ const { error } = await sb.rpc("mcp_emit_events", {
341
+ p_token_hash: getTokenHash(),
342
+ p_message_id: messageId,
343
+ p_events: eventsJson
413
344
  });
414
- const { error } = await sb.from("message_events").insert(rows);
415
345
  if (error) log.warn(`Failed to emit events batch: ${error.message}`);
416
346
  }
417
347
 
@@ -428,8 +358,6 @@ export {
428
358
  DAYBOX_AGENT_ID,
429
359
  getSupabase,
430
360
  loginWithToken,
431
- refreshWithCliToken,
432
- getSession,
433
361
  getCurrentUserId,
434
362
  logout,
435
363
  createSession,
@@ -439,6 +367,7 @@ export {
439
367
  getOrCreateCliConversation,
440
368
  createTask,
441
369
  pollPendingTasks,
370
+ pollAndClaimTask,
442
371
  claimTask,
443
372
  completeTask,
444
373
  failTask,
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- claimTask,
4
3
  clearConfig,
5
4
  completeTask,
6
5
  createSession,
@@ -17,14 +16,14 @@ import {
17
16
  loginWithToken,
18
17
  logout,
19
18
  newCorrelationId,
20
- pollPendingTasks,
19
+ pollAndClaimTask,
21
20
  resetEventSequence,
22
21
  setConfig,
23
22
  setCorrelationId,
24
23
  setLogLevel,
25
24
  setSessionBusy,
26
25
  updateHeartbeat
27
- } from "./chunk-VWSNGP65.js";
26
+ } from "./chunk-QXT7DH44.js";
28
27
 
29
28
  // src/index.ts
30
29
  import { Command } from "commander";
@@ -277,12 +276,11 @@ var SessionManager = class {
277
276
  return;
278
277
  }
279
278
  try {
280
- const tasks = await pollPendingTasks(this.session.id);
279
+ const task = await pollAndClaimTask(this.session.id);
281
280
  this.consecutivePollFailures = 0;
282
- if (tasks.length > 0) {
281
+ if (task) {
283
282
  this.processing = true;
284
- const task = tasks[0];
285
- log.info(`New task received: ${task.id}`);
283
+ log.info(`New task claimed: ${task.id}`);
286
284
  log.agent(`Task from conversation: ${task.conversation_id}`);
287
285
  await setSessionBusy(this.session.id, true);
288
286
  try {
@@ -2449,11 +2447,6 @@ var TaskProcessor = class {
2449
2447
  const toolCallRecords = [];
2450
2448
  const usedSkillNames = [];
2451
2449
  try {
2452
- await withRetry(() => claimTask(task.id), {
2453
- maxRetries: 2,
2454
- baseDelayMs: 300,
2455
- label: "claimTask"
2456
- });
2457
2450
  await emitEvent(task.id, "status_change", { status: "running" });
2458
2451
  let systemPrompt = BASE_SYSTEM_PROMPT.replace(
2459
2452
  "{workspace_path}",
@@ -3087,7 +3080,7 @@ program.command("start", { isDefault: true }).description("Start the agent and l
3087
3080
  }
3088
3081
  log.agent(`Processing: "${input}"`);
3089
3082
  try {
3090
- const { createTask: createTask2 } = await import("./supabase-XHDOQMOM.js");
3083
+ const { createTask: createTask2 } = await import("./supabase-QU7MFNDI.js");
3091
3084
  const session = sessionManager.getSession();
3092
3085
  const conversationId = sessionManager.getConversationId();
3093
3086
  if (session && conversationId) {
@@ -3116,7 +3109,7 @@ program.command("start", { isDefault: true }).description("Start the agent and l
3116
3109
  program.command("status").description("Check the status of the current agent session").action(async () => {
3117
3110
  try {
3118
3111
  const userId = await getCurrentUserId();
3119
- const { getSupabase: getSupabase2 } = await import("./supabase-XHDOQMOM.js");
3112
+ const { getSupabase: getSupabase2 } = await import("./supabase-QU7MFNDI.js");
3120
3113
  const sb = getSupabase2();
3121
3114
  const { data: sessions } = await sb.from("agent_sessions").select("*").eq("user_id", userId).in("status", ["online", "busy"]).order("started_at", { ascending: false }).limit(5);
3122
3115
  if (!sessions || sessions.length === 0) {
@@ -11,16 +11,15 @@ import {
11
11
  failTask,
12
12
  getCurrentUserId,
13
13
  getOrCreateCliConversation,
14
- getSession,
15
14
  getSupabase,
16
15
  loginWithToken,
17
16
  logout,
17
+ pollAndClaimTask,
18
18
  pollPendingTasks,
19
- refreshWithCliToken,
20
19
  resetEventSequence,
21
20
  setSessionBusy,
22
21
  updateHeartbeat
23
- } from "./chunk-VWSNGP65.js";
22
+ } from "./chunk-QXT7DH44.js";
24
23
  export {
25
24
  CLI_AGENT_ID,
26
25
  DAYBOX_AGENT_ID,
@@ -34,12 +33,11 @@ export {
34
33
  failTask,
35
34
  getCurrentUserId,
36
35
  getOrCreateCliConversation,
37
- getSession,
38
36
  getSupabase,
39
37
  loginWithToken,
40
38
  logout,
39
+ pollAndClaimTask,
41
40
  pollPendingTasks,
42
- refreshWithCliToken,
43
41
  resetEventSequence,
44
42
  setSessionBusy,
45
43
  updateHeartbeat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,6 @@ vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
19
19
 
20
20
  // Mock Supabase DB functions
21
21
  const mockDbFns = {
22
- claimTask: vi.fn().mockResolvedValue(undefined),
23
22
  completeTask: vi.fn().mockResolvedValue(undefined),
24
23
  failTask: vi.fn().mockResolvedValue(undefined),
25
24
  emitEvent: vi.fn().mockResolvedValue(undefined),
@@ -130,7 +129,7 @@ describe("TaskProcessor", () => {
130
129
 
131
130
  await processor.processTask(makeTask("say hello"));
132
131
 
133
- expect(mockDbFns.claimTask).toHaveBeenCalledWith("task-001");
132
+ // Task is already claimed by pollAndClaimTask in session.ts, not processor
134
133
  expect(mockDbFns.completeTask).toHaveBeenCalledWith(
135
134
  "task-001",
136
135
  expect.stringContaining("Hello!")
@@ -196,7 +195,7 @@ describe("TaskProcessor", () => {
196
195
  });
197
196
 
198
197
  it("handles task failure gracefully", async () => {
199
- mockDbFns.claimTask.mockRejectedValueOnce(new Error("DB error"));
198
+ mockDbFns.emitEvent.mockRejectedValueOnce(new Error("DB error"));
200
199
 
201
200
  await processor.processTask(makeTask("will fail"));
202
201
 
@@ -5,7 +5,6 @@ import {
5
5
  } from "@anthropic-ai/claude-agent-sdk";
6
6
  import {
7
7
  AgentTask,
8
- claimTask,
9
8
  completeTask,
10
9
  failTask,
11
10
  emitEvent,
@@ -108,12 +107,7 @@ export class TaskProcessor {
108
107
  const usedSkillNames: string[] = [];
109
108
 
110
109
  try {
111
- // Claim the task (with retry for transient DB failures)
112
- await withRetry(() => claimTask(task.id), {
113
- maxRetries: 2,
114
- baseDelayMs: 300,
115
- label: "claimTask",
116
- });
110
+ // Task is already claimed atomically by pollAndClaimTask in session.ts
117
111
  await emitEvent(task.id, "status_change", { status: "running" });
118
112
 
119
113
  // Build system prompt with memories + skills
@@ -30,7 +30,7 @@ const mockCreateSession = vi.fn().mockResolvedValue(mockSession);
30
30
  const mockUpdateHeartbeat = vi.fn().mockResolvedValue(undefined);
31
31
  const mockEndSession = vi.fn().mockResolvedValue(undefined);
32
32
  const mockSetSessionBusy = vi.fn().mockResolvedValue(undefined);
33
- const mockPollPendingTasks = vi.fn().mockResolvedValue([]);
33
+ const mockPollAndClaimTask = vi.fn().mockResolvedValue(null);
34
34
  const mockGetOrCreateCliConversation = vi.fn().mockResolvedValue("conv-001");
35
35
 
36
36
  vi.mock("../db/supabase.js", () => ({
@@ -38,7 +38,7 @@ vi.mock("../db/supabase.js", () => ({
38
38
  updateHeartbeat: (...args: any[]) => mockUpdateHeartbeat(...args),
39
39
  endSession: (...args: any[]) => mockEndSession(...args),
40
40
  setSessionBusy: (...args: any[]) => mockSetSessionBusy(...args),
41
- pollPendingTasks: (...args: any[]) => mockPollPendingTasks(...args),
41
+ pollAndClaimTask: (...args: any[]) => mockPollAndClaimTask(...args),
42
42
  createTask: vi.fn().mockResolvedValue({ id: "task-001", prompt: "test" }),
43
43
  getOrCreateCliConversation: (...args: any[]) => mockGetOrCreateCliConversation(...args),
44
44
  getSupabase: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue(chain) }),
@@ -79,7 +79,7 @@ describe("SessionManager", () => {
79
79
  mockUpdateHeartbeat.mockResolvedValue(undefined);
80
80
  mockEndSession.mockResolvedValue(undefined);
81
81
  mockSetSessionBusy.mockResolvedValue(undefined);
82
- mockPollPendingTasks.mockResolvedValue([]);
82
+ mockPollAndClaimTask.mockResolvedValue(null);
83
83
  vi.useFakeTimers({ shouldAdvanceTime: true });
84
84
  manager = new SessionManager();
85
85
  });
@@ -117,12 +117,12 @@ describe("SessionManager", () => {
117
117
 
118
118
  await vi.advanceTimersByTimeAsync(3_000);
119
119
 
120
- expect(mockPollPendingTasks).toHaveBeenCalledWith("sess-001");
120
+ expect(mockPollAndClaimTask).toHaveBeenCalledWith("sess-001");
121
121
  });
122
122
 
123
- it("processes tasks when polled", async () => {
123
+ it("processes tasks when polled and claimed atomically", async () => {
124
124
  const task = { id: "task-001", conversation_id: "conv-001", prompt: "hello" };
125
- mockPollPendingTasks.mockResolvedValueOnce([task]);
125
+ mockPollAndClaimTask.mockResolvedValueOnce(task);
126
126
 
127
127
  await manager.start("user-123", onTask);
128
128
  await vi.advanceTimersByTimeAsync(3_000);
@@ -133,24 +133,24 @@ describe("SessionManager", () => {
133
133
  });
134
134
 
135
135
  it("applies exponential backoff on poll failures", async () => {
136
- mockPollPendingTasks
136
+ mockPollAndClaimTask
137
137
  .mockRejectedValueOnce(new Error("Network error"))
138
138
  .mockRejectedValueOnce(new Error("Network error"))
139
- .mockResolvedValue([]);
139
+ .mockResolvedValue(null);
140
140
 
141
141
  await manager.start("user-123", onTask);
142
142
 
143
143
  // First poll at ~2s base interval
144
144
  await vi.advanceTimersByTimeAsync(3_000);
145
- expect(mockPollPendingTasks).toHaveBeenCalledTimes(1);
145
+ expect(mockPollAndClaimTask).toHaveBeenCalledTimes(1);
146
146
 
147
147
  // After first failure: backoff = 2s * 2^1 = 4s → fires at ~6s total
148
148
  await vi.advanceTimersByTimeAsync(4_000);
149
- expect(mockPollPendingTasks).toHaveBeenCalledTimes(2);
149
+ expect(mockPollAndClaimTask).toHaveBeenCalledTimes(2);
150
150
 
151
151
  // After second failure: backoff = 2s * 2^2 = 8s → fires at ~14s total
152
152
  await vi.advanceTimersByTimeAsync(8_500);
153
- expect(mockPollPendingTasks).toHaveBeenCalledTimes(3);
153
+ expect(mockPollAndClaimTask).toHaveBeenCalledTimes(3);
154
154
  // Third call succeeds, so backoff resets. Next poll at 2s would be at ~16s
155
155
  // We don't advance that far, so only 3 calls total.
156
156
  });
@@ -3,7 +3,7 @@ import {
3
3
  updateHeartbeat,
4
4
  endSession,
5
5
  setSessionBusy,
6
- pollPendingTasks,
6
+ pollAndClaimTask,
7
7
  createTask,
8
8
  getOrCreateCliConversation,
9
9
  getSupabase,
@@ -145,14 +145,15 @@ export class SessionManager {
145
145
  }
146
146
 
147
147
  try {
148
- const tasks = await pollPendingTasks(this.session.id);
148
+ // Atomic poll + claim: if a pending task exists, it's claimed in one DB call.
149
+ // FOR UPDATE SKIP LOCKED ensures no two CLIs grab the same task.
150
+ const task = await pollAndClaimTask(this.session.id);
149
151
  // Reset backoff on success
150
152
  this.consecutivePollFailures = 0;
151
153
 
152
- if (tasks.length > 0) {
154
+ if (task) {
153
155
  this.processing = true;
154
- const task = tasks[0];
155
- log.info(`New task received: ${task.id}`);
156
+ log.info(`New task claimed: ${task.id}`);
156
157
  log.agent(`Task from conversation: ${task.conversation_id}`);
157
158
 
158
159
  await setSessionBusy(this.session.id, true);
@@ -11,8 +11,7 @@ import { homedir } from "os";
11
11
  export const CLI_AGENT_ID = "00000000-0000-0000-0000-000000000001";
12
12
  export const DAYBOX_AGENT_ID = "00000000-0000-0000-0000-000000000002";
13
13
 
14
- // ── File-Based Storage Adapter ─────────────────────────────────────
15
- // Node.js has no localStorage, so we persist Supabase auth tokens to disk.
14
+ // ── Auth Store (persists am_ token to disk) ──────────────────────────
16
15
 
17
16
  const AUTH_DIR = join(homedir(), ".config", "assistme");
18
17
  const AUTH_FILE = join(AUTH_DIR, "auth.json");
@@ -39,28 +38,7 @@ function writeAuthStore(data: Record<string, string>) {
39
38
  writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
40
39
  }
41
40
 
42
- /**
43
- * Custom storage adapter for Supabase client.
44
- * Persists auth sessions to ~/.config/assistme/auth.json
45
- */
46
- const fileStorage = {
47
- getItem(key: string): string | null {
48
- const store = readAuthStore();
49
- return store[key] ?? null;
50
- },
51
- setItem(key: string, value: string): void {
52
- const store = readAuthStore();
53
- store[key] = value;
54
- writeAuthStore(store);
55
- },
56
- removeItem(key: string): void {
57
- const store = readAuthStore();
58
- delete store[key];
59
- writeAuthStore(store);
60
- },
61
- };
62
-
63
- // ── Supabase Client ────────────────────────────────────────────────
41
+ // ── Supabase Client (anon key only, no auth session) ────────────────
64
42
 
65
43
  let supabase: SupabaseClient | null = null;
66
44
 
@@ -73,47 +51,33 @@ export function getSupabase(): SupabaseClient {
73
51
  );
74
52
  }
75
53
  supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
76
- auth: {
77
- storage: fileStorage,
78
- autoRefreshToken: false, // We handle refresh via am_ token
79
- persistSession: true,
80
- },
54
+ auth: { persistSession: false },
81
55
  });
82
56
  }
83
57
  return supabase;
84
58
  }
85
59
 
86
- // ── Auth: Token-Based Login ────────────────────────────────────────
60
+ // ── Token Hash ──────────────────────────────────────────────────────
87
61
 
88
- /** SHA-256 hex hash of a token string. */
89
62
  function hashToken(token: string): string {
90
63
  return createHash("sha256").update(token).digest("hex");
91
64
  }
92
65
 
93
- /**
94
- * Validate an am_ token via RPC and get a signed JWT.
95
- * No edge function needed — calls login_with_mcp_token directly.
96
- */
97
- async function authenticateWithMcpToken(
98
- mcpToken: string
99
- ): Promise<{ access_token: string; user_id: string }> {
100
- const tokenHash = hashToken(mcpToken);
101
- const sb = getSupabase();
102
-
103
- const { data, error } = await sb.rpc("login_with_mcp_token", {
104
- p_token_hash: tokenHash,
105
- });
106
-
107
- if (error) throw new Error(`Token validation failed: ${error.message}`);
108
- if (data?.error) throw new Error(data.error);
109
- if (!data?.access_token) throw new Error("Token validation returned no session");
110
-
111
- return data;
66
+ /** Get stored token hash (computed from persisted am_ token). */
67
+ function getTokenHash(): string {
68
+ const store = readAuthStore();
69
+ const token = store["mcp_token"];
70
+ if (!token || !token.startsWith("am_")) {
71
+ throw new Error("Not authenticated. Run `assistme login`.");
72
+ }
73
+ return hashToken(token);
112
74
  }
113
75
 
76
+ // ── Auth ─────────────────────────────────────────────────────────────
77
+
114
78
  /**
115
79
  * Login using an am_ MCP token.
116
- * Validates via DB, gets a signed JWT, sets the Supabase session.
80
+ * Validates against DB via validate_mcp_token RPC, stores locally.
117
81
  */
118
82
  export async function loginWithToken(mcpToken: string): Promise<string> {
119
83
  if (!mcpToken.startsWith("am_")) {
@@ -122,70 +86,37 @@ export async function loginWithToken(mcpToken: string): Promise<string> {
122
86
  );
123
87
  }
124
88
 
125
- const { access_token, user_id } = await authenticateWithMcpToken(mcpToken);
126
-
127
- // Persist the MCP token so we can re-authenticate later
128
- const store = readAuthStore();
129
- store["mcp_token"] = mcpToken;
130
- writeAuthStore(store);
131
-
89
+ const hash = hashToken(mcpToken);
132
90
  const sb = getSupabase();
133
- const { data, error } = await sb.auth.setSession({
134
- access_token,
135
- refresh_token: "mcp_managed", // placeholder; we refresh via am_ token
136
- });
137
91
 
138
- if (error) throw new Error(`Login failed: ${error.message}`);
139
- if (!data.user) throw new Error("Login failed: no user returned");
92
+ const { data, error } = await sb.rpc("validate_mcp_token", {
93
+ p_token_hash: hash,
94
+ });
140
95
 
141
- return data.user.id;
142
- }
96
+ if (error) throw new Error(`Token validation failed: ${error.message}`);
97
+ if (!data || data.length === 0) throw new Error("Invalid or expired token");
143
98
 
144
- /**
145
- * Re-authenticate using the stored MCP token (am_).
146
- * Called automatically when the Supabase session has expired.
147
- */
148
- export async function refreshWithCliToken(): Promise<string | null> {
99
+ // Persist token
149
100
  const store = readAuthStore();
150
- const mcpToken = store["mcp_token"];
151
- if (!mcpToken || !mcpToken.startsWith("am_")) return null;
152
-
153
- try {
154
- const { access_token } = await authenticateWithMcpToken(mcpToken);
155
- const sb = getSupabase();
156
- const { data, error } = await sb.auth.setSession({
157
- access_token,
158
- refresh_token: "mcp_managed",
159
- });
160
- if (error || !data.user) return null;
161
- return data.user.id;
162
- } catch {
163
- return null;
164
- }
165
- }
101
+ store["mcp_token"] = mcpToken;
102
+ writeAuthStore(store);
166
103
 
167
- export async function getSession() {
168
- const sb = getSupabase();
169
- const { data } = await sb.auth.getSession();
170
- return data.session;
104
+ return data[0].out_user_id;
171
105
  }
172
106
 
173
107
  export async function getCurrentUserId(): Promise<string> {
108
+ const hash = getTokenHash();
174
109
  const sb = getSupabase();
175
- const { data, error } = await sb.auth.getUser();
176
- if (error || !data.user) {
177
- // Try auto-refresh with stored CLI token
178
- const refreshed = await refreshWithCliToken();
179
- if (refreshed) return refreshed;
180
- throw new Error("Not authenticated. Run `assistme login`.");
110
+ const { data, error } = await sb.rpc("validate_mcp_token", {
111
+ p_token_hash: hash,
112
+ });
113
+ if (error || !data || data.length === 0) {
114
+ throw new Error("Token expired or revoked. Run `assistme login`.");
181
115
  }
182
- return data.user.id;
116
+ return data[0].out_user_id;
183
117
  }
184
118
 
185
119
  export async function logout(): Promise<void> {
186
- const sb = getSupabase();
187
- await sb.auth.signOut();
188
- // Also clear the file
189
120
  try {
190
121
  writeAuthStore({});
191
122
  } catch {
@@ -209,24 +140,19 @@ export interface AgentSession {
209
140
  }
210
141
 
211
142
  export async function createSession(
212
- userId: string,
143
+ _userId: string,
213
144
  sessionName: string,
214
145
  workspacePath: string,
215
146
  version: string
216
147
  ): Promise<AgentSession> {
217
148
  const sb = getSupabase();
218
- const { data, error } = await sb
219
- .from("agent_sessions")
220
- .insert({
221
- user_id: userId,
222
- session_name: sessionName,
223
- status: "online",
224
- workspace_path: workspacePath,
225
- agent_version: version,
226
- metadata: { model: getConfig().model },
227
- })
228
- .select()
229
- .single();
149
+ const { data, error } = await sb.rpc("mcp_create_session", {
150
+ p_token_hash: getTokenHash(),
151
+ p_session_name: sessionName,
152
+ p_workspace_path: workspacePath,
153
+ p_version: version,
154
+ p_model: getConfig().model || null,
155
+ });
230
156
 
231
157
  if (error) throw new Error(`Failed to create session: ${error.message}`);
232
158
  return data as AgentSession;
@@ -234,24 +160,19 @@ export async function createSession(
234
160
 
235
161
  export async function updateHeartbeat(sessionId: string): Promise<void> {
236
162
  const sb = getSupabase();
237
- const { error } = await sb
238
- .from("agent_sessions")
239
- .update({ last_heartbeat_at: new Date().toISOString() })
240
- .eq("id", sessionId);
241
-
163
+ const { error } = await sb.rpc("mcp_heartbeat", {
164
+ p_token_hash: getTokenHash(),
165
+ p_session_id: sessionId,
166
+ });
242
167
  if (error) log.warn(`Heartbeat update failed: ${error.message}`);
243
168
  }
244
169
 
245
170
  export async function endSession(sessionId: string): Promise<void> {
246
171
  const sb = getSupabase();
247
- const { error } = await sb
248
- .from("agent_sessions")
249
- .update({
250
- status: "offline",
251
- ended_at: new Date().toISOString(),
252
- })
253
- .eq("id", sessionId);
254
-
172
+ const { error } = await sb.rpc("mcp_end_session", {
173
+ p_token_hash: getTokenHash(),
174
+ p_session_id: sessionId,
175
+ });
255
176
  if (error) log.error(`Failed to end session: ${error.message}`);
256
177
  }
257
178
 
@@ -260,64 +181,28 @@ export async function setSessionBusy(
260
181
  busy: boolean
261
182
  ): Promise<void> {
262
183
  const sb = getSupabase();
263
- await sb
264
- .from("agent_sessions")
265
- .update({ status: busy ? "busy" : "online" })
266
- .eq("id", sessionId);
184
+ await sb.rpc("mcp_set_session_busy", {
185
+ p_token_hash: getTokenHash(),
186
+ p_session_id: sessionId,
187
+ p_busy: busy,
188
+ });
267
189
  }
268
190
 
269
191
  // ── Conversation Management ─────────────────────────────────────────
270
192
 
271
- /**
272
- * Find or create a CLI conversation for a given session.
273
- * Each agent session gets one conversation.
274
- */
275
193
  export async function getOrCreateCliConversation(
276
- userId: string,
277
- sessionId: string
194
+ _userId: string,
195
+ _sessionId: string
278
196
  ): Promise<string> {
279
197
  const sb = getSupabase();
280
-
281
- // Check if a CLI conversation already exists for this user's current session
282
- // by looking for conversations where the agent is 'cli' and user is participant
283
- const { data: existing } = await sb
284
- .from("conversations")
285
- .select("id")
286
- .eq("agent", "cli")
287
- .eq("created_by", userId)
288
- .order("created_at", { ascending: false })
289
- .limit(1);
290
-
291
- if (existing && existing.length > 0) {
292
- return existing[0].id;
293
- }
294
-
295
- // Create new conversation
296
- const { data: conv, error: convErr } = await sb
297
- .from("conversations")
298
- .insert({
299
- conversation_type: "direct",
300
- agent: "cli",
301
- created_by: userId,
302
- })
303
- .select("id")
304
- .single();
305
-
306
- if (convErr || !conv) {
307
- throw new Error(`Failed to create conversation: ${convErr?.message}`);
308
- }
309
-
310
- // Add participants: user + CLI agent
311
- await sb.from("conversation_participants").insert([
312
- { conversation_id: conv.id, user_id: userId, role: "member" },
313
- { conversation_id: conv.id, user_id: CLI_AGENT_ID, role: "member" },
314
- ]);
315
-
316
- return conv.id;
198
+ const { data, error } = await sb.rpc("mcp_get_or_create_conversation", {
199
+ p_token_hash: getTokenHash(),
200
+ });
201
+ if (error) throw new Error(`Failed to get conversation: ${error.message}`);
202
+ return data as string;
317
203
  }
318
204
 
319
205
  // ── Message / Task Management ────────────────────────────────────────
320
- // A "task" is now an assistant message in conversation_messages with a status.
321
206
 
322
207
  export interface ConversationMessage {
323
208
  id: string;
@@ -332,115 +217,88 @@ export interface ConversationMessage {
332
217
  metadata: Record<string, unknown>;
333
218
  created_at: string;
334
219
  updated_at: string;
335
- /** The original user prompt (stored in metadata for assistant messages) */
336
220
  prompt: string;
337
221
  }
338
222
 
339
- // Keep AgentTask as an alias for backward compatibility with processor.ts
340
223
  export type AgentTask = ConversationMessage;
341
224
 
342
- /**
343
- * Create a user prompt + pending assistant message pair atomically.
344
- * Uses a PG function for transactional safety. Falls back to sequential
345
- * inserts if the function doesn't exist yet.
346
- * Returns the assistant message (the "task" to process).
347
- */
348
225
  export async function createTask(
349
226
  conversationId: string,
350
- userId: string,
227
+ _userId: string,
351
228
  sessionId: string,
352
229
  prompt: string
353
230
  ): Promise<ConversationMessage> {
354
231
  const sb = getSupabase();
355
-
356
- // Try atomic RPC first
357
- const { data: rpcResult, error: rpcError } = await sb.rpc(
358
- "create_task_pair",
359
- {
360
- p_conversation_id: conversationId,
361
- p_user_id: userId,
362
- p_agent_id: CLI_AGENT_ID,
363
- p_session_id: sessionId,
364
- p_prompt: prompt,
365
- }
366
- );
367
-
368
- if (!rpcError && rpcResult) {
369
- return { ...rpcResult, prompt } as ConversationMessage;
370
- }
371
-
372
- // Fallback: sequential inserts (for environments without the PG function)
373
- if (rpcError) {
374
- log.debug(`create_task_pair RPC unavailable, using fallback: ${rpcError.message}`);
375
- }
376
-
377
- // Insert user message
378
- const { error: userErr } = await sb.from("conversation_messages").insert({
379
- conversation_id: conversationId,
380
- sender_id: userId,
381
- role: "user",
382
- content: prompt,
232
+ const { data, error } = await sb.rpc("mcp_create_task", {
233
+ p_token_hash: getTokenHash(),
234
+ p_conversation_id: conversationId,
235
+ p_session_id: sessionId,
236
+ p_prompt: prompt,
383
237
  });
384
238
 
385
- if (userErr) throw new Error(`Failed to create user message: ${userErr.message}`);
386
-
387
- // Insert assistant placeholder (the "task")
388
- const { data, error } = await sb
389
- .from("conversation_messages")
390
- .insert({
391
- conversation_id: conversationId,
392
- sender_id: CLI_AGENT_ID,
393
- role: "assistant",
394
- content: "",
395
- status: "pending",
396
- session_id: sessionId,
397
- metadata: { prompt },
398
- })
399
- .select()
400
- .single();
401
-
402
239
  if (error) throw new Error(`Failed to create task: ${error.message}`);
403
240
  return { ...data, prompt } as ConversationMessage;
404
241
  }
405
242
 
406
- /**
407
- * Poll for pending assistant messages assigned to this session.
408
- */
409
243
  export async function pollPendingTasks(
410
244
  sessionId: string
411
245
  ): Promise<ConversationMessage[]> {
412
246
  const sb = getSupabase();
413
- const { data, error } = await sb
414
- .from("conversation_messages")
415
- .select("*")
416
- .eq("session_id", sessionId)
417
- .eq("status", "pending")
418
- .order("created_at", { ascending: true })
419
- .limit(1);
247
+ const { data, error } = await sb.rpc("mcp_poll_tasks", {
248
+ p_token_hash: getTokenHash(),
249
+ p_session_id: sessionId,
250
+ });
420
251
 
421
252
  if (error) {
422
253
  log.warn(`Task poll failed: ${error.message}`);
423
254
  return [];
424
255
  }
425
- // Extract prompt from metadata for backward compat with processor
426
- return (data || []).map((row: Record<string, unknown>) => ({
256
+
257
+ const rows = (data || []) as Record<string, unknown>[];
258
+ return rows.map((row) => ({
427
259
  ...row,
428
- prompt: (row.metadata as Record<string, unknown>)?.prompt || row.content || "",
260
+ prompt:
261
+ (row.metadata as Record<string, unknown>)?.prompt || row.content || "",
429
262
  })) as ConversationMessage[];
430
263
  }
431
264
 
432
- export async function claimTask(messageId: string): Promise<void> {
265
+ /**
266
+ * Atomically poll ONE pending task and claim it in a single DB call.
267
+ * Uses FOR UPDATE SKIP LOCKED — concurrent CLIs will never grab the same task.
268
+ * Returns null if no pending task exists.
269
+ */
270
+ export async function pollAndClaimTask(
271
+ sessionId: string
272
+ ): Promise<ConversationMessage | null> {
433
273
  const sb = getSupabase();
434
- const { error } = await sb
435
- .from("conversation_messages")
436
- .update({
437
- status: "running",
438
- metadata: { started_at: new Date().toISOString() },
439
- })
440
- .eq("id", messageId)
441
- .eq("status", "pending");
274
+ const { data, error } = await sb.rpc("mcp_poll_and_claim_task", {
275
+ p_token_hash: getTokenHash(),
276
+ p_session_id: sessionId,
277
+ });
278
+
279
+ if (error) {
280
+ log.warn(`Poll-and-claim failed: ${error.message}`);
281
+ return null;
282
+ }
283
+
284
+ if (!data) return null;
285
+
286
+ const row = data as Record<string, unknown>;
287
+ return {
288
+ ...row,
289
+ prompt:
290
+ (row.metadata as Record<string, unknown>)?.prompt || row.content || "",
291
+ } as ConversationMessage;
292
+ }
442
293
 
294
+ export async function claimTask(messageId: string): Promise<boolean> {
295
+ const sb = getSupabase();
296
+ const { data, error } = await sb.rpc("mcp_claim_task", {
297
+ p_token_hash: getTokenHash(),
298
+ p_message_id: messageId,
299
+ });
443
300
  if (error) throw new Error(`Failed to claim task: ${error.message}`);
301
+ return data as boolean;
444
302
  }
445
303
 
446
304
  export async function completeTask(
@@ -449,29 +307,12 @@ export async function completeTask(
449
307
  tokenUsage?: Record<string, number>
450
308
  ): Promise<void> {
451
309
  const sb = getSupabase();
452
-
453
- // Fetch current metadata to merge
454
- const { data: current } = await sb
455
- .from("conversation_messages")
456
- .select("metadata")
457
- .eq("id", messageId)
458
- .single();
459
-
460
- const existingMeta = (current?.metadata as Record<string, unknown>) || {};
461
-
462
- const { error } = await sb
463
- .from("conversation_messages")
464
- .update({
465
- content: resultSummary,
466
- status: "completed",
467
- metadata: {
468
- ...existingMeta,
469
- completed_at: new Date().toISOString(),
470
- token_usage: tokenUsage || null,
471
- },
472
- })
473
- .eq("id", messageId);
474
-
310
+ const { error } = await sb.rpc("mcp_complete_task", {
311
+ p_token_hash: getTokenHash(),
312
+ p_message_id: messageId,
313
+ p_result: resultSummary,
314
+ p_token_usage: tokenUsage || null,
315
+ });
475
316
  if (error) throw new Error(`Failed to complete task: ${error.message}`);
476
317
  }
477
318
 
@@ -480,28 +321,11 @@ export async function failTask(
480
321
  errorMessage: string
481
322
  ): Promise<void> {
482
323
  const sb = getSupabase();
483
-
484
- const { data: current } = await sb
485
- .from("conversation_messages")
486
- .select("metadata")
487
- .eq("id", messageId)
488
- .single();
489
-
490
- const existingMeta = (current?.metadata as Record<string, unknown>) || {};
491
-
492
- const { error } = await sb
493
- .from("conversation_messages")
494
- .update({
495
- status: "failed",
496
- content: errorMessage,
497
- metadata: {
498
- ...existingMeta,
499
- completed_at: new Date().toISOString(),
500
- error_message: errorMessage,
501
- },
502
- })
503
- .eq("id", messageId);
504
-
324
+ const { error } = await sb.rpc("mcp_fail_task", {
325
+ p_token_hash: getTokenHash(),
326
+ p_message_id: messageId,
327
+ p_error: errorMessage,
328
+ });
505
329
  if (error) log.error(`Failed to update task status: ${error.message}`);
506
330
  }
507
331
 
@@ -529,32 +353,29 @@ export async function emitEvent(
529
353
  ): Promise<void> {
530
354
  const sb = getSupabase();
531
355
  eventSequence++;
532
- const { error } = await sb.from("message_events").insert({
533
- message_id: messageId,
534
- event_type: eventType,
535
- event_data: eventData,
536
- sequence_number: eventSequence,
356
+ const { error } = await sb.rpc("mcp_emit_event", {
357
+ p_token_hash: getTokenHash(),
358
+ p_message_id: messageId,
359
+ p_event_type: eventType,
360
+ p_event_data: eventData,
361
+ p_seq: eventSequence,
537
362
  });
538
-
539
363
  if (error) log.warn(`Failed to emit event: ${error.message}`);
540
364
  }
541
365
 
542
- // Batch emit for efficiency
543
366
  export async function emitEvents(
544
367
  messageId: string,
545
368
  events: Array<{ type: EventType; data: Record<string, unknown> }>
546
369
  ): Promise<void> {
547
370
  const sb = getSupabase();
548
- const rows = events.map((e) => {
371
+ const eventsJson = events.map((e) => {
549
372
  eventSequence++;
550
- return {
551
- message_id: messageId,
552
- event_type: e.type,
553
- event_data: e.data,
554
- sequence_number: eventSequence,
555
- };
373
+ return { type: e.type, data: e.data, seq: eventSequence };
374
+ });
375
+ const { error } = await sb.rpc("mcp_emit_events", {
376
+ p_token_hash: getTokenHash(),
377
+ p_message_id: messageId,
378
+ p_events: eventsJson,
556
379
  });
557
-
558
- const { error } = await sb.from("message_events").insert(rows);
559
380
  if (error) log.warn(`Failed to emit events batch: ${error.message}`);
560
381
  }