assistme 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ import { MemoryManager } from "./memory.js";
22
22
  import { SkillManager } from "./skills.js";
23
23
  import { type ToolCallRecord } from "./skill-extractor.js";
24
24
  import { evaluateAndMaybeCreateSkill } from "./skill-evaluator.js";
25
+ import { analyzeSelfPostTask } from "./self-analyzer.js";
25
26
  import { withRetry } from "../utils/retry.js";
26
27
  import {
27
28
  createBrowserMcpServer,
@@ -355,11 +356,30 @@ export class TaskProcessor {
355
356
  );
356
357
  }
357
358
 
358
- // Post-task: resume the same session to evaluate skill creation
359
+ // Post-task: skill evaluation self-analysis (serial to avoid session resume conflicts)
359
360
  if (agentSessionId) {
360
- this.evaluateSkillPostTask(agentSessionId, config.model).catch((err) =>
361
- log.debug(`Post-task skill evaluation skipped: ${err}`)
362
- );
361
+ (async () => {
362
+ try {
363
+ await this.evaluateSkillPostTask(agentSessionId, config.model);
364
+ } catch (err) {
365
+ log.debug(`Post-task skill evaluation skipped: ${err}`);
366
+ }
367
+ try {
368
+ await analyzeSelfPostTask({
369
+ model: config.model,
370
+ taskId: task.id,
371
+ conversationId: task.conversation_id,
372
+ taskPrompt: task.prompt,
373
+ taskResponse: finalResponse,
374
+ toolCallRecords,
375
+ toolFailures,
376
+ tokenUsage,
377
+ sessionId: this.sessionId || "",
378
+ });
379
+ } catch (err) {
380
+ log.debug(`Post-task self-analysis skipped: ${err}`);
381
+ }
382
+ })().catch(() => {});
363
383
  }
364
384
  } catch (err) {
365
385
  const errMsg = errorMessage(err);
@@ -0,0 +1,444 @@
1
+ import {
2
+ query,
3
+ type SDKResultMessage,
4
+ type SDKResultSuccess,
5
+ type OutputFormat,
6
+ } from "@anthropic-ai/claude-agent-sdk";
7
+ import { submitFeedback, FeedbackError } from "edsger-feedback";
8
+ import { log } from "../utils/logger.js";
9
+ import {
10
+ SelfAnalysisResultSchema,
11
+ type SelfAnalysisResult,
12
+ safeParse,
13
+ } from "../utils/schemas.js";
14
+ import { errorMessage } from "../utils/errors.js";
15
+ import {
16
+ getSessionLogs,
17
+ getMessageEvents,
18
+ getConversationMessages,
19
+ } from "../db/analysis-data.js";
20
+ import {
21
+ SELF_ANALYSIS_MAX_SESSION_LOGS,
22
+ SELF_ANALYSIS_MAX_MESSAGE_EVENTS,
23
+ SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES,
24
+ SELF_ANALYSIS_LOG_CONTEXT_CHARS,
25
+ SELF_ANALYSIS_EVENT_CONTEXT_CHARS,
26
+ EDSGER_PRODUCT_SLUG,
27
+ } from "../utils/constants.js";
28
+ import type { ToolCallRecord } from "./skill-extractor.js";
29
+ import type { ToolFailureRecord } from "./event-hooks.js";
30
+
31
+ // ── Structured Output Schema ────────────────────────────────────
32
+
33
+ const SELF_ANALYSIS_OUTPUT_FORMAT: OutputFormat = {
34
+ type: "json_schema",
35
+ schema: {
36
+ type: "object",
37
+ properties: {
38
+ is_perfect: { type: "boolean" },
39
+ overall_score: { type: "number" },
40
+ task_completion_quality: {
41
+ type: "object",
42
+ properties: {
43
+ score: { type: "number" },
44
+ assessment: { type: "string" },
45
+ },
46
+ required: ["score", "assessment"],
47
+ },
48
+ improvements: {
49
+ type: "array",
50
+ items: {
51
+ type: "object",
52
+ properties: {
53
+ area: { type: "string" },
54
+ severity: {
55
+ type: "string",
56
+ enum: ["critical", "major", "minor", "suggestion"],
57
+ },
58
+ description: { type: "string" },
59
+ suggestion: { type: "string" },
60
+ },
61
+ required: ["area", "severity", "description", "suggestion"],
62
+ },
63
+ },
64
+ data_quality: {
65
+ type: "object",
66
+ properties: {
67
+ session_logs_useful: { type: "boolean" },
68
+ session_logs_gaps: { type: "string" },
69
+ message_events_useful: { type: "boolean" },
70
+ message_events_gaps: { type: "string" },
71
+ conversation_context_useful: { type: "boolean" },
72
+ conversation_context_gaps: { type: "string" },
73
+ },
74
+ required: [
75
+ "session_logs_useful",
76
+ "message_events_useful",
77
+ "conversation_context_useful",
78
+ ],
79
+ },
80
+ summary: { type: "string" },
81
+ },
82
+ required: [
83
+ "is_perfect",
84
+ "overall_score",
85
+ "task_completion_quality",
86
+ "improvements",
87
+ "data_quality",
88
+ "summary",
89
+ ],
90
+ },
91
+ };
92
+
93
+ // ── Analysis Prompt ─────────────────────────────────────────────
94
+
95
+ const SELF_ANALYSIS_PROMPT = `You just completed a task as the AssistMe agent. Now critically analyze AssistMe's own implementation — NOT the user's task itself, but how well AssistMe (the agent system) performed and whether AssistMe's codebase can be improved.
96
+
97
+ ## Your Role
98
+ You are a perfectionist code reviewer analyzing the AssistMe agent system itself. Be critical, thorough, and constructive. Focus on:
99
+
100
+ 1. **Task Completion Quality**: Did AssistMe handle the task optimally? Were there unnecessary steps, missed edge cases, or suboptimal tool usage?
101
+ 2. **Agent Architecture**: Based on what you observed during execution, are there architectural improvements to AssistMe's code (processor, event hooks, MCP servers, skill system, etc.)?
102
+ 3. **Data & Observability**: Evaluate the quality of the context data provided (session logs, message events, conversation messages). What information is missing that would help diagnose issues or improve the system?
103
+ 4. **Error Handling & Resilience**: Were there any failures or retries? Could the error handling be improved?
104
+ 5. **Performance & Efficiency**: Were there unnecessary API calls, redundant operations, or opportunities to optimize?
105
+ 6. **User Experience**: Could the interaction flow be smoother? Is the feedback to the user adequate?
106
+
107
+ ## Context Data Provided
108
+ Below you will find:
109
+ - **Session Logs**: stdout/stderr/status logs from the agent session
110
+ - **Message Events**: Real-time events emitted during task execution (tool calls, results, status changes, thinking blocks)
111
+ - **Conversation Messages**: User interaction history for this conversation
112
+ - **Tool Call Records**: Summary of all tool calls made during the task
113
+ - **Tool Failures**: Any tool calls that failed during execution
114
+
115
+ ## Instructions
116
+ Analyze all provided data critically. Consider:
117
+ - Are the session logs capturing enough detail for debugging?
118
+ - Do the message events provide sufficient visibility into the agent's decision-making?
119
+ - Is the conversation context giving enough user intent signal?
120
+ - Were tools used efficiently?
121
+ - Could the overall execution flow be improved?
122
+
123
+ Set is_perfect to true ONLY if there are genuinely zero improvements to suggest (this should be rare).
124
+ The overall_score should be 1-10 where 10 means absolutely perfect.
125
+
126
+ Respond with a JSON object now.`;
127
+
128
+ // ── Context Building ────────────────────────────────────────────
129
+
130
+ interface SelfAnalysisContext {
131
+ taskId: string;
132
+ conversationId: string;
133
+ sessionId: string;
134
+ taskPrompt: string;
135
+ taskResponse: string;
136
+ toolCallRecords: ToolCallRecord[];
137
+ toolFailures: ToolFailureRecord[];
138
+ tokenUsage?: Record<string, number>;
139
+ }
140
+
141
+ function truncateToChars(text: string, maxChars: number): string {
142
+ if (text.length <= maxChars) return text;
143
+ return text.slice(0, maxChars) + "\n... [truncated]";
144
+ }
145
+
146
+ /** Max characters for tool result in event summary */
147
+ const EVENT_RESULT_SUMMARY_CHARS = 150;
148
+
149
+ /**
150
+ * Aggregate and filter events intelligently:
151
+ * - Merge consecutive text_delta events into a single entry
152
+ * - Summarize tool_result data (keep name + truncated result)
153
+ * - Keep tool_failure, status_change, error events in full
154
+ * - Drop thinking events (verbose, low signal for analysis)
155
+ */
156
+ function filterAndAggregateEvents(
157
+ events: Array<{ event_type: string; event_data: Record<string, unknown>; sequence_number: number; created_at: string }>
158
+ ): string[] {
159
+ const lines: string[] = [];
160
+ let pendingTextChunks: string[] = [];
161
+
162
+ const flushTextDelta = () => {
163
+ if (pendingTextChunks.length > 0) {
164
+ const merged = pendingTextChunks.join("");
165
+ const summary = merged.length > 200
166
+ ? merged.slice(0, 200) + "..."
167
+ : merged;
168
+ lines.push(`[text_delta x${pendingTextChunks.length}] ${summary}`);
169
+ pendingTextChunks = [];
170
+ }
171
+ };
172
+
173
+ for (const e of events) {
174
+ switch (e.event_type) {
175
+ case "text_delta": {
176
+ const text = (e.event_data.text as string) || "";
177
+ pendingTextChunks.push(text);
178
+ break;
179
+ }
180
+
181
+ case "thinking":
182
+ // Skip thinking events — verbose and low signal for self-analysis
183
+ flushTextDelta();
184
+ break;
185
+
186
+ case "tool_use_input":
187
+ // Skip raw input — tool_use_start + tool_result cover the essentials
188
+ flushTextDelta();
189
+ break;
190
+
191
+ case "tool_result": {
192
+ flushTextDelta();
193
+ const name = (e.event_data.name as string) || "unknown";
194
+ const result = ((e.event_data.result as string) || "").slice(0, EVENT_RESULT_SUMMARY_CHARS);
195
+ lines.push(`[tool_result] ${name}: ${result}`);
196
+ break;
197
+ }
198
+
199
+ case "tool_failure":
200
+ case "status_change":
201
+ case "error": {
202
+ flushTextDelta();
203
+ lines.push(`[${e.event_type}] ${JSON.stringify(e.event_data)}`);
204
+ break;
205
+ }
206
+
207
+ default: {
208
+ flushTextDelta();
209
+ const dataStr = JSON.stringify(e.event_data);
210
+ lines.push(`[${e.event_type}] ${dataStr.slice(0, 200)}`);
211
+ break;
212
+ }
213
+ }
214
+ }
215
+
216
+ flushTextDelta();
217
+ return lines;
218
+ }
219
+
220
+ async function buildAnalysisContext(ctx: SelfAnalysisContext): Promise<string> {
221
+ // Fetch all data sources in parallel
222
+ const [sessionLogs, messageEvents, conversationMessages] = await Promise.all([
223
+ getSessionLogs(ctx.sessionId, SELF_ANALYSIS_MAX_SESSION_LOGS),
224
+ getMessageEvents(ctx.taskId, SELF_ANALYSIS_MAX_MESSAGE_EVENTS),
225
+ getConversationMessages(ctx.conversationId, SELF_ANALYSIS_MAX_CONVERSATION_MESSAGES),
226
+ ]);
227
+
228
+ let context = "";
229
+
230
+ // Session Logs
231
+ if (sessionLogs.length > 0) {
232
+ const logText = sessionLogs
233
+ .map((l) => `[${l.log_type}] ${l.message}`)
234
+ .join("\n");
235
+ context += `\n## Session Logs (${sessionLogs.length} entries)\n`;
236
+ context += truncateToChars(logText, SELF_ANALYSIS_LOG_CONTEXT_CHARS);
237
+ } else {
238
+ context += "\n## Session Logs\n(No session logs available — this is itself a data gap to note)\n";
239
+ }
240
+
241
+ // Message Events (filtered & aggregated)
242
+ if (messageEvents.length > 0) {
243
+ const filteredLines = filterAndAggregateEvents(messageEvents);
244
+ const eventText = filteredLines.join("\n");
245
+ context += `\n## Message Events (${messageEvents.length} raw → ${filteredLines.length} aggregated)\n`;
246
+ context += truncateToChars(eventText, SELF_ANALYSIS_EVENT_CONTEXT_CHARS);
247
+ } else {
248
+ context += "\n## Message Events\n(No message events available — this is itself a data gap to note)\n";
249
+ }
250
+
251
+ // Conversation Messages
252
+ if (conversationMessages.length > 0) {
253
+ context += `\n## Conversation Messages (${conversationMessages.length} entries)\n`;
254
+ for (const msg of conversationMessages) {
255
+ const role = (msg.role as string) || "unknown";
256
+ const content = ((msg.content as string) || "").slice(0, 500);
257
+ const status = (msg.status as string) || "";
258
+ context += `[${role}${status ? ` (${status})` : ""}] ${content}\n`;
259
+ }
260
+ } else {
261
+ context += "\n## Conversation Messages\n(No conversation messages available)\n";
262
+ }
263
+
264
+ // Tool Call Records
265
+ if (ctx.toolCallRecords.length > 0) {
266
+ context += `\n## Tool Call Records (${ctx.toolCallRecords.length} calls)\n`;
267
+ for (const tc of ctx.toolCallRecords) {
268
+ const inputStr = JSON.stringify(tc.input).slice(0, 200);
269
+ context += `- ${tc.name}: ${inputStr} → ${tc.result.slice(0, 100)}\n`;
270
+ }
271
+ }
272
+
273
+ // Tool Failures
274
+ if (ctx.toolFailures.length > 0) {
275
+ context += `\n## Tool Failures (${ctx.toolFailures.length} failures)\n`;
276
+ for (const tf of ctx.toolFailures) {
277
+ context += `- ${tf.toolName}: ${tf.error}\n`;
278
+ }
279
+ }
280
+
281
+ // Token Usage
282
+ if (ctx.tokenUsage) {
283
+ context += `\n## Token Usage\n`;
284
+ context += `Input: ${ctx.tokenUsage.input_tokens}, Output: ${ctx.tokenUsage.output_tokens}\n`;
285
+ }
286
+
287
+ // Task Summary
288
+ context += `\n## Task\n`;
289
+ context += `Prompt: ${ctx.taskPrompt.slice(0, 500)}\n`;
290
+ context += `Response length: ${ctx.taskResponse.length} chars\n`;
291
+
292
+ return context;
293
+ }
294
+
295
+ // ── Feedback Submission ─────────────────────────────────────────
296
+
297
+ async function submitSelfAnalysisFeedback(analysis: SelfAnalysisResult): Promise<void> {
298
+ const title = `Self-Analysis: Score ${analysis.overall_score}/10 — ${analysis.improvements.length} improvement(s)`;
299
+
300
+ const improvementDetails = analysis.improvements
301
+ .map((imp, i) => `${i + 1}. [${imp.severity}] **${imp.area}**: ${imp.description}\n → ${imp.suggestion}`)
302
+ .join("\n");
303
+
304
+ const dataQualityNotes = [
305
+ analysis.data_quality.session_logs_gaps
306
+ ? `Session logs: ${analysis.data_quality.session_logs_gaps}`
307
+ : null,
308
+ analysis.data_quality.message_events_gaps
309
+ ? `Message events: ${analysis.data_quality.message_events_gaps}`
310
+ : null,
311
+ analysis.data_quality.conversation_context_gaps
312
+ ? `Conversation context: ${analysis.data_quality.conversation_context_gaps}`
313
+ : null,
314
+ ]
315
+ .filter(Boolean)
316
+ .join("\n");
317
+
318
+ let description = `## Summary\n${analysis.summary}\n\n`;
319
+ description += `## Task Completion Quality (${analysis.task_completion_quality.score}/10)\n${analysis.task_completion_quality.assessment}\n\n`;
320
+ description += `## Improvements\n${improvementDetails}\n`;
321
+
322
+ if (dataQualityNotes) {
323
+ description += `\n## Data Quality Gaps\n${dataQualityNotes}\n`;
324
+ }
325
+
326
+ // Truncate to fit edsger-feedback's 5000 char limit
327
+ if (description.length > 4900) {
328
+ description = description.slice(0, 4900) + "\n...[truncated]";
329
+ }
330
+
331
+ try {
332
+ const result = await submitFeedback({
333
+ slug: EDSGER_PRODUCT_SLUG,
334
+ title: title.slice(0, 200),
335
+ description,
336
+ category: "improvement",
337
+ });
338
+ log.info(`Self-analysis feedback submitted: ${result.id}`);
339
+ } catch (err) {
340
+ if (err instanceof FeedbackError) {
341
+ log.debug(`Feedback submission failed (${err.statusCode}): ${err.message}`);
342
+ } else {
343
+ log.debug(`Feedback submission failed: ${errorMessage(err)}`);
344
+ }
345
+ }
346
+ }
347
+
348
+ // ── Main Entry Point ────────────────────────────────────────────
349
+
350
+ /**
351
+ * Post-task self-analysis: resume the agent SDK session to critically
352
+ * analyze AssistMe's own performance and code quality.
353
+ *
354
+ * If improvements are found, submits feedback via edsger-feedback.
355
+ * If the analysis determines everything is perfect, no feedback is sent.
356
+ */
357
+ export async function analyzeSelfPostTask(opts: {
358
+ model: string;
359
+ taskId: string;
360
+ conversationId: string;
361
+ taskPrompt: string;
362
+ taskResponse: string;
363
+ toolCallRecords: ToolCallRecord[];
364
+ toolFailures: ToolFailureRecord[];
365
+ tokenUsage?: Record<string, number>;
366
+ sessionId: string;
367
+ }): Promise<void> {
368
+ const {
369
+ model,
370
+ taskId,
371
+ conversationId,
372
+ taskPrompt,
373
+ taskResponse,
374
+ toolCallRecords,
375
+ toolFailures,
376
+ tokenUsage,
377
+ sessionId,
378
+ } = opts;
379
+
380
+ try {
381
+ // Build context from all available data sources
382
+ const analysisContext = await buildAnalysisContext({
383
+ taskId,
384
+ conversationId,
385
+ sessionId,
386
+ taskPrompt,
387
+ taskResponse,
388
+ toolCallRecords,
389
+ toolFailures,
390
+ tokenUsage,
391
+ });
392
+
393
+ const prompt = `${SELF_ANALYSIS_PROMPT}\n${analysisContext}\n\nRespond with a JSON object now.`;
394
+
395
+ let structuredOutput: unknown;
396
+
397
+ // Use independent query() instead of session resume to avoid
398
+ // conflicts with skill evaluation which also resumes the session
399
+ for await (const message of query({
400
+ prompt,
401
+ options: {
402
+ model,
403
+ maxTurns: 1,
404
+ allowedTools: [],
405
+ effort: "low",
406
+ outputFormat: SELF_ANALYSIS_OUTPUT_FORMAT,
407
+ },
408
+ })) {
409
+ if (message.type === "result") {
410
+ const resultMsg = message as SDKResultMessage;
411
+ if (resultMsg.subtype === "success") {
412
+ const successMsg = resultMsg as SDKResultSuccess;
413
+ structuredOutput = successMsg.structured_output;
414
+ log.debug(
415
+ `Self-analysis cost: $${successMsg.total_cost_usd.toFixed(4)}`
416
+ );
417
+ }
418
+ }
419
+ }
420
+
421
+ // Validate against Zod schema
422
+ const analysis = structuredOutput
423
+ ? safeParse(SelfAnalysisResultSchema, structuredOutput)
424
+ : null;
425
+
426
+ if (!analysis) {
427
+ log.debug("Self-analysis: no valid structured output");
428
+ return;
429
+ }
430
+
431
+ log.info(
432
+ `Self-analysis complete: score=${analysis.overall_score}/10, perfect=${analysis.is_perfect}, improvements=${analysis.improvements.length}`
433
+ );
434
+
435
+ // Only submit feedback if not perfect
436
+ if (!analysis.is_perfect && analysis.improvements.length > 0) {
437
+ await submitSelfAnalysisFeedback(analysis);
438
+ } else {
439
+ log.debug("Self-analysis: no improvements to report — skipping feedback");
440
+ }
441
+ } catch (err) {
442
+ log.debug(`Self-analysis error: ${errorMessage(err)}`);
443
+ }
444
+ }
@@ -4,10 +4,11 @@ import ora from "ora";
4
4
  import { createInterface } from "readline";
5
5
  import { getCurrentUserId } from "../db/supabase.js";
6
6
  import { setConfig } from "../utils/config.js";
7
- import { log, setLogLevel } from "../utils/logger.js";
7
+ import { log, setLogLevel, setLogHook } from "../utils/logger.js";
8
8
  import { SessionManager } from "../agent/session.js";
9
9
  import { TaskProcessor } from "../agent/processor.js";
10
10
  import { getBrowser, ensureBrowserAvailable } from "../tools/browser.js";
11
+ import { SessionLogEmitter } from "../db/session-log.js";
11
12
 
12
13
  export function registerStartCommand(program: Command): void {
13
14
  program
@@ -85,12 +86,19 @@ async function runAgent(opts: { workspace?: string; name?: string; verbose?: boo
85
86
  const processor = new TaskProcessor();
86
87
  processor.init(userId);
87
88
  const sessionManager = new SessionManager();
89
+ let logEmitter: SessionLogEmitter | null = null;
88
90
 
89
91
  // Graceful shutdown
90
92
  const browserRef = getBrowser();
91
93
  const shutdown = async () => {
92
94
  console.log();
93
95
  log.info("Shutting down...");
96
+ setLogHook(null);
97
+ try {
98
+ if (logEmitter) await logEmitter.stop();
99
+ } catch {
100
+ /* ignore */
101
+ }
94
102
  try {
95
103
  if (browserRef.isConnected()) await browserRef.disconnect();
96
104
  } catch {
@@ -109,6 +117,12 @@ async function runAgent(opts: { workspace?: string; name?: string; verbose?: boo
109
117
  });
110
118
  processor.setSessionId(session.id);
111
119
 
120
+ // Start persisting logs to Supabase
121
+ logEmitter = new SessionLogEmitter(session.id);
122
+ setLogHook((logType, message) => {
123
+ logEmitter?.push(logType, message);
124
+ });
125
+
112
126
  log.info("Listening for tasks (chat + jobs) from web UI...");
113
127
  log.info("Press Ctrl+C to stop.\n");
114
128
 
@@ -0,0 +1,79 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+ import { log } from "../utils/logger.js";
3
+
4
+ export interface SessionLogEntry {
5
+ log_type: "stdout" | "stderr" | "status";
6
+ message: string;
7
+ sequence_number: number;
8
+ created_at: string;
9
+ }
10
+
11
+ export interface MessageEventEntry {
12
+ event_type: string;
13
+ event_data: Record<string, unknown>;
14
+ sequence_number: number;
15
+ created_at: string;
16
+ }
17
+
18
+ /**
19
+ * Fetch agent session logs for a given session.
20
+ * Uses the log.list MCP handler endpoint.
21
+ */
22
+ export async function getSessionLogs(
23
+ sessionId: string,
24
+ limit = 500
25
+ ): Promise<SessionLogEntry[]> {
26
+ try {
27
+ const data = await callMcpHandler<SessionLogEntry[]>("log.list", {
28
+ session_id: sessionId,
29
+ limit,
30
+ });
31
+ return data || [];
32
+ } catch (err) {
33
+ log.debug(`Failed to fetch session logs: ${err instanceof Error ? err.message : err}`);
34
+ return [];
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Fetch message events for a given task (message_id).
40
+ * Uses the event.list MCP handler endpoint.
41
+ */
42
+ export async function getMessageEvents(
43
+ messageId: string,
44
+ limit = 500
45
+ ): Promise<MessageEventEntry[]> {
46
+ try {
47
+ const data = await callMcpHandler<MessageEventEntry[]>("event.list", {
48
+ message_id: messageId,
49
+ limit,
50
+ });
51
+ return data || [];
52
+ } catch (err) {
53
+ log.debug(`Failed to fetch message events: ${err instanceof Error ? err.message : err}`);
54
+ return [];
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Fetch conversation messages for context.
60
+ * Returns the most recent messages including the current task.
61
+ */
62
+ export async function getConversationMessages(
63
+ conversationId: string,
64
+ limit = 10
65
+ ): Promise<Array<Record<string, unknown>>> {
66
+ try {
67
+ const data = await callMcpHandler<Array<Record<string, unknown>>>(
68
+ "conversation.list_messages",
69
+ {
70
+ conversation_id: conversationId,
71
+ limit,
72
+ }
73
+ );
74
+ return data || [];
75
+ } catch (err) {
76
+ log.debug(`Failed to fetch conversation messages: ${err instanceof Error ? err.message : err}`);
77
+ return [];
78
+ }
79
+ }
@@ -0,0 +1,71 @@
1
+ import { callMcpHandler } from "./api-client.js";
2
+ import { log } from "../utils/logger.js";
3
+
4
+ const FLUSH_INTERVAL_MS = 3_000;
5
+ const MAX_BATCH_SIZE = 100;
6
+
7
+ interface PendingLog {
8
+ log_type: "stdout" | "stderr" | "status";
9
+ message: string;
10
+ seq: number;
11
+ }
12
+
13
+ /**
14
+ * Batches agent process logs and flushes them to Supabase periodically.
15
+ * Each session gets its own emitter with an auto-incrementing sequence.
16
+ */
17
+ export class SessionLogEmitter {
18
+ private sequence = 0;
19
+ private buffer: PendingLog[] = [];
20
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
21
+ private flushing = false;
22
+
23
+ constructor(private sessionId: string) {
24
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
25
+ }
26
+
27
+ /** Queue a log entry for batch insertion */
28
+ push(logType: "stdout" | "stderr" | "status", message: string): void {
29
+ this.sequence++;
30
+ this.buffer.push({ log_type: logType, message, seq: this.sequence });
31
+
32
+ // Flush immediately if buffer is large
33
+ if (this.buffer.length >= MAX_BATCH_SIZE) {
34
+ this.flush();
35
+ }
36
+ }
37
+
38
+ /** Flush buffered logs to Supabase */
39
+ async flush(): Promise<void> {
40
+ if (this.flushing || this.buffer.length === 0) return;
41
+
42
+ const batch = this.buffer.splice(0);
43
+ this.flushing = true;
44
+
45
+ try {
46
+ await callMcpHandler("log.emit_batch", {
47
+ session_id: this.sessionId,
48
+ logs: batch,
49
+ });
50
+ } catch (err) {
51
+ log.debug(
52
+ `Failed to flush session logs: ${err instanceof Error ? err.message : err}`
53
+ );
54
+ // Re-queue failed batch at the front (best-effort, drop if too old)
55
+ if (this.buffer.length < MAX_BATCH_SIZE * 5) {
56
+ this.buffer.unshift(...batch);
57
+ }
58
+ } finally {
59
+ this.flushing = false;
60
+ }
61
+ }
62
+
63
+ /** Stop the emitter and flush remaining logs */
64
+ async stop(): Promise<void> {
65
+ if (this.flushTimer) {
66
+ clearInterval(this.flushTimer);
67
+ this.flushTimer = null;
68
+ }
69
+ await this.flush();
70
+ }
71
+ }