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.
- package/dist/{chunk-VWSNGP65.js → chunk-QXT7DH44.js} +109 -180
- package/dist/index.js +7 -14
- package/dist/{supabase-XHDOQMOM.js → supabase-QU7MFNDI.js} +3 -5
- package/package.json +1 -1
- package/src/agent/processor.test.ts +2 -3
- package/src/agent/processor.ts +1 -7
- package/src/agent/session.test.ts +11 -11
- package/src/agent/session.ts +6 -5
- package/src/db/supabase.ts +133 -312
|
@@ -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
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
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
|
|
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.
|
|
203
|
-
|
|
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(`
|
|
208
|
-
if (!data.
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
throw new Error("
|
|
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.
|
|
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(
|
|
202
|
+
async function createSession(_userId, sessionName, workspacePath, version) {
|
|
252
203
|
const sb = getSupabase();
|
|
253
|
-
const { data, error } = await sb.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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.
|
|
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.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
})
|
|
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.
|
|
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(
|
|
238
|
+
async function getOrCreateCliConversation(_userId, _sessionId) {
|
|
282
239
|
const sb = getSupabase();
|
|
283
|
-
const { data
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
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,
|
|
246
|
+
async function createTask(conversationId, _userId, sessionId, prompt) {
|
|
302
247
|
const sb = getSupabase();
|
|
303
|
-
const { data
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
})
|
|
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 {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
336
|
+
const eventsJson = events.map((e) => {
|
|
406
337
|
eventSequence++;
|
|
407
|
-
return {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
19
|
+
pollAndClaimTask,
|
|
21
20
|
resetEventSequence,
|
|
22
21
|
setConfig,
|
|
23
22
|
setCorrelationId,
|
|
24
23
|
setLogLevel,
|
|
25
24
|
setSessionBusy,
|
|
26
25
|
updateHeartbeat
|
|
27
|
-
} from "./chunk-
|
|
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
|
|
279
|
+
const task = await pollAndClaimTask(this.session.id);
|
|
281
280
|
this.consecutivePollFailures = 0;
|
|
282
|
-
if (
|
|
281
|
+
if (task) {
|
|
283
282
|
this.processing = true;
|
|
284
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
@@ -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
|
-
|
|
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.
|
|
198
|
+
mockDbFns.emitEvent.mockRejectedValueOnce(new Error("DB error"));
|
|
200
199
|
|
|
201
200
|
await processor.processTask(makeTask("will fail"));
|
|
202
201
|
|
package/src/agent/processor.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
});
|
package/src/agent/session.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
updateHeartbeat,
|
|
4
4
|
endSession,
|
|
5
5
|
setSessionBusy,
|
|
6
|
-
|
|
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
|
-
|
|
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 (
|
|
154
|
+
if (task) {
|
|
153
155
|
this.processing = true;
|
|
154
|
-
|
|
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);
|
package/src/db/supabase.ts
CHANGED
|
@@ -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
|
-
// ──
|
|
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
|
-
// ──
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
92
|
+
const { data, error } = await sb.rpc("validate_mcp_token", {
|
|
93
|
+
p_token_hash: hash,
|
|
94
|
+
});
|
|
140
95
|
|
|
141
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
277
|
-
|
|
194
|
+
_userId: string,
|
|
195
|
+
_sessionId: string
|
|
278
196
|
): Promise<string> {
|
|
279
197
|
const sb = getSupabase();
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
227
|
+
_userId: string,
|
|
351
228
|
sessionId: string,
|
|
352
229
|
prompt: string
|
|
353
230
|
): Promise<ConversationMessage> {
|
|
354
231
|
const sb = getSupabase();
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
426
|
-
|
|
256
|
+
|
|
257
|
+
const rows = (data || []) as Record<string, unknown>[];
|
|
258
|
+
return rows.map((row) => ({
|
|
427
259
|
...row,
|
|
428
|
-
prompt:
|
|
260
|
+
prompt:
|
|
261
|
+
(row.metadata as Record<string, unknown>)?.prompt || row.content || "",
|
|
429
262
|
})) as ConversationMessage[];
|
|
430
263
|
}
|
|
431
264
|
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
.
|
|
441
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
371
|
+
const eventsJson = events.map((e) => {
|
|
549
372
|
eventSequence++;
|
|
550
|
-
return {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
}
|