mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-11 00:02:38 +02:00
show errors in activity tab for knowledge graph
This commit is contained in:
parent
c382e3ee8a
commit
c6083de054
6 changed files with 171 additions and 34 deletions
|
|
@ -156,6 +156,28 @@ const SERVICE_LABELS: Record<string, string> = {
|
||||||
granola: "Syncing Granola",
|
granola: "Syncing Granola",
|
||||||
graph: "Updating knowledge",
|
graph: "Updating knowledge",
|
||||||
voice_memo: "Processing voice memo",
|
voice_memo: "Processing voice memo",
|
||||||
|
email_labeling: "Labeling emails",
|
||||||
|
note_tagging: "Tagging notes",
|
||||||
|
agent_notes: "Updating agent notes",
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeServiceError(error: string): string {
|
||||||
|
const firstLine = error.split("\n").find((line) => line.trim().length > 0)
|
||||||
|
return firstLine?.trim() || error.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectServiceErrors(events: ServiceEventType[]): Map<string, string> {
|
||||||
|
const errors = new Map<string, string>()
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.type === "error") {
|
||||||
|
errors.set(event.service, summarizeServiceError(event.error))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (event.type === "run_complete" && event.outcome !== "error") {
|
||||||
|
errors.delete(event.service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
type TasksActions = {
|
type TasksActions = {
|
||||||
|
|
@ -227,6 +249,7 @@ function formatRunTime(ts: string): string {
|
||||||
function SyncStatusBar() {
|
function SyncStatusBar() {
|
||||||
const { state } = useSidebar()
|
const { state } = useSidebar()
|
||||||
const [activeServices, setActiveServices] = useState<Map<string, string>>(new Map())
|
const [activeServices, setActiveServices] = useState<Map<string, string>>(new Map())
|
||||||
|
const [serviceErrors, setServiceErrors] = useState<Map<string, string>>(new Map())
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false)
|
const [popoverOpen, setPopoverOpen] = useState(false)
|
||||||
const [logEvents, setLogEvents] = useState<ServiceEventType[]>([])
|
const [logEvents, setLogEvents] = useState<ServiceEventType[]>([])
|
||||||
const [logLoading, setLogLoading] = useState(false)
|
const [logLoading, setLogLoading] = useState(false)
|
||||||
|
|
@ -260,11 +283,25 @@ function SyncStatusBar() {
|
||||||
next.delete(nextEvent.runId)
|
next.delete(nextEvent.runId)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
if (nextEvent.outcome !== 'error') {
|
||||||
|
setServiceErrors((prev) => {
|
||||||
|
if (!prev.has(nextEvent.service)) return prev
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.delete(nextEvent.service)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
const existingTimeout = runTimeoutsRef.current.get(nextEvent.runId)
|
const existingTimeout = runTimeoutsRef.current.get(nextEvent.runId)
|
||||||
if (existingTimeout) {
|
if (existingTimeout) {
|
||||||
clearTimeout(existingTimeout)
|
clearTimeout(existingTimeout)
|
||||||
runTimeoutsRef.current.delete(nextEvent.runId)
|
runTimeoutsRef.current.delete(nextEvent.runId)
|
||||||
}
|
}
|
||||||
|
} else if (nextEvent.type === 'error') {
|
||||||
|
setServiceErrors((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(nextEvent.service, summarizeServiceError(nextEvent.error))
|
||||||
|
return next
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return cleanup
|
return cleanup
|
||||||
|
|
@ -298,10 +335,14 @@ function SyncStatusBar() {
|
||||||
// skip malformed lines
|
// skip malformed lines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setServiceErrors(collectServiceErrors(parsed))
|
||||||
// Newest first, limit to 1000
|
// Newest first, limit to 1000
|
||||||
setLogEvents(parsed.reverse().slice(0, MAX_SYNC_EVENTS))
|
setLogEvents(parsed.reverse().slice(0, MAX_SYNC_EVENTS))
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setLogEvents([])
|
if (!cancelled) {
|
||||||
|
setLogEvents([])
|
||||||
|
setServiceErrors(new Map())
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setLogLoading(false)
|
if (!cancelled) setLogLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -312,12 +353,19 @@ function SyncStatusBar() {
|
||||||
|
|
||||||
const isSyncing = activeServices.size > 0
|
const isSyncing = activeServices.size > 0
|
||||||
const isCollapsed = state === "collapsed"
|
const isCollapsed = state === "collapsed"
|
||||||
|
const errorEntries = Array.from(serviceErrors.entries())
|
||||||
|
const primaryErrorService = errorEntries[0]?.[0] ?? null
|
||||||
|
const hasServiceErrors = errorEntries.length > 0
|
||||||
|
|
||||||
// Build status label from active services
|
// Build status label from active services
|
||||||
const activeServiceNames = [...new Set(activeServices.values())]
|
const activeServiceNames = [...new Set(activeServices.values())]
|
||||||
const statusLabel = isSyncing
|
const statusLabel = isSyncing
|
||||||
? activeServiceNames.map((s) => SERVICE_LABELS[s] || s).join(", ")
|
? activeServiceNames.map((s) => SERVICE_LABELS[s] || s).join(", ")
|
||||||
: "All caught up"
|
: hasServiceErrors
|
||||||
|
? errorEntries.length === 1
|
||||||
|
? `${SERVICE_LABELS[primaryErrorService ?? ""] || primaryErrorService} failed`
|
||||||
|
: "Recent sync issues"
|
||||||
|
: "All caught up"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -335,11 +383,16 @@ function SyncStatusBar() {
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-sidebar-accent"
|
className={cn(
|
||||||
|
"flex w-full items-center justify-between rounded-md px-2 py-1 text-xs hover:bg-sidebar-accent",
|
||||||
|
hasServiceErrors && !isSyncing ? "text-red-600 dark:text-red-400" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 min-w-0">
|
<span className="flex items-center gap-2 min-w-0">
|
||||||
{isSyncing ? (
|
{isSyncing ? (
|
||||||
<LoaderIcon className="h-3 w-3 shrink-0 animate-spin" />
|
<LoaderIcon className="h-3 w-3 shrink-0 animate-spin" />
|
||||||
|
) : hasServiceErrors ? (
|
||||||
|
<AlertTriangle className="h-3 w-3 shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/60" />
|
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/60" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -357,7 +410,7 @@ function SyncStatusBar() {
|
||||||
<div className="p-3 border-b">
|
<div className="p-3 border-b">
|
||||||
<h4 className="font-semibold text-sm">Sync Activity</h4>
|
<h4 className="font-semibold text-sm">Sync Activity</h4>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{isSyncing ? statusLabel : "All services up to date"}
|
{isSyncing || hasServiceErrors ? statusLabel : "All services up to date"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-80 overflow-y-auto p-2">
|
<div className="max-h-80 overflow-y-auto p-2">
|
||||||
|
|
@ -389,7 +442,17 @@ function SyncStatusBar() {
|
||||||
{SERVICE_LABELS[event.service]?.split(" ").slice(-1)[0] || event.service}
|
{SERVICE_LABELS[event.service]?.split(" ").slice(-1)[0] || event.service}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="leading-4 text-foreground/80">{event.message}</span>
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="leading-4 text-foreground/80">{event.message}</p>
|
||||||
|
{event.type === 'error' && (
|
||||||
|
<p
|
||||||
|
className="truncate text-[11px] leading-4 text-red-600/90 dark:text-red-400/90"
|
||||||
|
title={event.error}
|
||||||
|
>
|
||||||
|
{summarizeServiceError(event.error)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,35 @@
|
||||||
import { bus } from "../runs/bus.js";
|
import { bus } from "../runs/bus.js";
|
||||||
import { fetchRun } from "../runs/runs.js";
|
import { fetchRun } from "../runs/runs.js";
|
||||||
|
|
||||||
|
type RunRecord = Awaited<ReturnType<typeof fetchRun>>;
|
||||||
|
|
||||||
|
function extractRunErrors(run: RunRecord): string[] {
|
||||||
|
return run.log.flatMap((event) => event.type === "error" ? [event.error] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RunFailedError extends Error {
|
||||||
|
readonly runId: string;
|
||||||
|
readonly errors: string[];
|
||||||
|
|
||||||
|
constructor(runId: string, errors: string[]) {
|
||||||
|
const firstError = errors.find(Boolean) ?? null;
|
||||||
|
super(firstError ? `Run ${runId} failed: ${firstError}` : `Run ${runId} failed`);
|
||||||
|
this.name = "RunFailedError";
|
||||||
|
this.runId = runId;
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorDetails(error: unknown): string {
|
||||||
|
if (error instanceof RunFailedError) {
|
||||||
|
return error.errors.join("\n\n");
|
||||||
|
}
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the assistant's final text response from a run's log.
|
* Extract the assistant's final text response from a run's log.
|
||||||
* @param runId
|
* @param runId
|
||||||
|
|
@ -28,13 +57,28 @@ export async function extractAgentResponse(runId: string): Promise<string | null
|
||||||
/**
|
/**
|
||||||
* Wait for a run to complete by listening for run-processing-end event
|
* Wait for a run to complete by listening for run-processing-end event
|
||||||
*/
|
*/
|
||||||
export async function waitForRunCompletion(runId: string): Promise<void> {
|
export async function waitForRunCompletion(
|
||||||
return new Promise(async (resolve) => {
|
runId: string,
|
||||||
const unsubscribe = await bus.subscribe('*', async (event) => {
|
opts: { throwOnError?: boolean } = {},
|
||||||
if (event.type === 'run-processing-end' && event.runId === runId) {
|
): Promise<RunRecord> {
|
||||||
unsubscribe();
|
return new Promise((resolve, reject) => {
|
||||||
resolve();
|
void (async () => {
|
||||||
}
|
const unsubscribe = await bus.subscribe('*', async (event) => {
|
||||||
});
|
if (event.type === 'run-processing-end' && event.runId === runId) {
|
||||||
|
unsubscribe();
|
||||||
|
try {
|
||||||
|
const run = await fetchRun(runId);
|
||||||
|
const errors = extractRunErrors(run);
|
||||||
|
if (opts.throwOnError && errors.length > 0) {
|
||||||
|
reject(new RunFailedError(runId, errors));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(run);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})().catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { google } from 'googleapis';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
import { getKgModel } from '../models/defaults.js';
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { getErrorDetails, waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger } from '../services/service_logger.js';
|
||||||
import { loadUserConfig, updateUserEmail } from '../config/user_config.js';
|
import { loadUserConfig, updateUserEmail } from '../config/user_config.js';
|
||||||
import { GoogleClientFactory } from './google-client-factory.js';
|
import { GoogleClientFactory } from './google-client-factory.js';
|
||||||
|
|
@ -288,7 +288,7 @@ async function processAgentNotes(): Promise<void> {
|
||||||
subUseCase: 'agent_notes',
|
subUseCase: 'agent_notes',
|
||||||
});
|
});
|
||||||
await createMessage(agentRun.id, message);
|
await createMessage(agentRun.id, message);
|
||||||
await waitForRunCompletion(agentRun.id);
|
await waitForRunCompletion(agentRun.id, { throwOnError: true });
|
||||||
|
|
||||||
// Mark everything as processed
|
// Mark everything as processed
|
||||||
for (const p of emailPaths) {
|
for (const p of emailPaths) {
|
||||||
|
|
@ -326,7 +326,16 @@ async function processAgentNotes(): Promise<void> {
|
||||||
runId: serviceRun.runId,
|
runId: serviceRun.runId,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: 'Error processing agent notes',
|
message: 'Error processing agent notes',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorDetails(error),
|
||||||
|
});
|
||||||
|
await serviceLogger.log({
|
||||||
|
type: 'run_complete',
|
||||||
|
service: serviceRun.service,
|
||||||
|
runId: serviceRun.runId,
|
||||||
|
level: 'error',
|
||||||
|
message: 'Agent notes processing failed',
|
||||||
|
durationMs: Date.now() - serviceRun.startedAt,
|
||||||
|
outcome: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import path from 'path';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
import { bus } from '../runs/bus.js';
|
import { bus } from '../runs/bus.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { getErrorDetails, waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger, type ServiceRunContext } from '../services/service_logger.js';
|
import { serviceLogger, type ServiceRunContext } from '../services/service_logger.js';
|
||||||
import {
|
import {
|
||||||
loadState,
|
loadState,
|
||||||
|
|
@ -312,8 +312,11 @@ async function createNotesFromBatch(
|
||||||
await createMessage(run.id, message);
|
await createMessage(run.id, message);
|
||||||
|
|
||||||
// Wait for the run to complete
|
// Wait for the run to complete
|
||||||
await waitForRunCompletion(run.id);
|
try {
|
||||||
unsubscribe();
|
await waitForRunCompletion(run.id, { throwOnError: true });
|
||||||
|
} finally {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
return { runId: run.id, notesCreated, notesModified };
|
return { runId: run.id, notesCreated, notesModified };
|
||||||
}
|
}
|
||||||
|
|
@ -428,7 +431,7 @@ async function buildGraphWithFiles(
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Error processing batch ${batchNumber}`,
|
message: `Error processing batch ${batchNumber}`,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorDetails(error),
|
||||||
context: { batchNumber },
|
context: { batchNumber },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -600,7 +603,7 @@ async function processVoiceMemosForKnowledge(): Promise<boolean> {
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Error processing voice memo batch ${batchNumber}`,
|
message: `Error processing voice memo batch ${batchNumber}`,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorDetails(error),
|
||||||
context: { batchNumber },
|
context: { batchNumber },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
import { getKgModel } from '../models/defaults.js';
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { bus } from '../runs/bus.js';
|
import { bus } from '../runs/bus.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { getErrorDetails, waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger } from '../services/service_logger.js';
|
||||||
import { limitEventItems } from './limit_event_items.js';
|
import { limitEventItems } from './limit_event_items.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -112,8 +112,11 @@ async function labelEmailBatch(
|
||||||
});
|
});
|
||||||
|
|
||||||
await createMessage(run.id, message);
|
await createMessage(run.id, message);
|
||||||
await waitForRunCompletion(run.id);
|
try {
|
||||||
unsubscribe();
|
await waitForRunCompletion(run.id, { throwOnError: true });
|
||||||
|
} finally {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
return { runId: run.id, filesEdited };
|
return { runId: run.id, filesEdited };
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +178,7 @@ export async function processUnlabeledEmails(concurrency: number = DEFAULT_CONCU
|
||||||
const totalBatches = batches.length;
|
const totalBatches = batches.length;
|
||||||
let totalEdited = 0;
|
let totalEdited = 0;
|
||||||
let hadError = false;
|
let hadError = false;
|
||||||
|
let failedBatches = 0;
|
||||||
|
|
||||||
// Process batches with concurrency limit
|
// Process batches with concurrency limit
|
||||||
for (let i = 0; i < batches.length; i += concurrency) {
|
for (let i = 0; i < batches.length; i += concurrency) {
|
||||||
|
|
@ -209,14 +213,16 @@ export async function processUnlabeledEmails(concurrency: number = DEFAULT_CONCU
|
||||||
return result.filesEdited.size;
|
return result.filesEdited.size;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
|
failedBatches++;
|
||||||
|
const errorDetails = getErrorDetails(error);
|
||||||
console.error(`[EmailLabeling] Error processing batch ${batchNumber}:`, error);
|
console.error(`[EmailLabeling] Error processing batch ${batchNumber}:`, error);
|
||||||
await serviceLogger.log({
|
await serviceLogger.log({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
service: run.service,
|
service: run.service,
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Error processing batch ${batchNumber}`,
|
message: `Email labeling batch ${batchNumber}/${totalBatches} failed`,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: errorDetails,
|
||||||
context: { batchNumber },
|
context: { batchNumber },
|
||||||
});
|
});
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -238,12 +244,15 @@ export async function processUnlabeledEmails(concurrency: number = DEFAULT_CONCU
|
||||||
service: run.service,
|
service: run.service,
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
level: hadError ? 'error' : 'info',
|
level: hadError ? 'error' : 'info',
|
||||||
message: `Email labeling complete: ${totalEdited} files labeled`,
|
message: hadError
|
||||||
|
? `Email labeling finished with errors: ${totalEdited} files labeled`
|
||||||
|
: `Email labeling complete: ${totalEdited} files labeled`,
|
||||||
durationMs: Date.now() - run.startedAt,
|
durationMs: Date.now() - run.startedAt,
|
||||||
outcome: hadError ? 'error' : 'ok',
|
outcome: hadError ? 'error' : 'ok',
|
||||||
summary: {
|
summary: {
|
||||||
totalEmails: unlabeled.length,
|
totalEmails: unlabeled.length,
|
||||||
filesLabeled: totalEdited,
|
filesLabeled: totalEdited,
|
||||||
|
failedBatches,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
import { getKgModel } from '../models/defaults.js';
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { bus } from '../runs/bus.js';
|
import { bus } from '../runs/bus.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { getErrorDetails, waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger } from '../services/service_logger.js';
|
||||||
import { limitEventItems } from './limit_event_items.js';
|
import { limitEventItems } from './limit_event_items.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -125,8 +125,11 @@ async function tagNoteBatch(
|
||||||
});
|
});
|
||||||
|
|
||||||
await createMessage(run.id, message);
|
await createMessage(run.id, message);
|
||||||
await waitForRunCompletion(run.id);
|
try {
|
||||||
unsubscribe();
|
await waitForRunCompletion(run.id, { throwOnError: true });
|
||||||
|
} finally {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
return { runId: run.id, filesEdited };
|
return { runId: run.id, filesEdited };
|
||||||
}
|
}
|
||||||
|
|
@ -169,6 +172,7 @@ export async function processUntaggedNotes(): Promise<void> {
|
||||||
const totalBatches = Math.ceil(untagged.length / BATCH_SIZE);
|
const totalBatches = Math.ceil(untagged.length / BATCH_SIZE);
|
||||||
let totalEdited = 0;
|
let totalEdited = 0;
|
||||||
let hadError = false;
|
let hadError = false;
|
||||||
|
let failedBatches = 0;
|
||||||
|
|
||||||
for (let i = 0; i < untagged.length; i += BATCH_SIZE) {
|
for (let i = 0; i < untagged.length; i += BATCH_SIZE) {
|
||||||
const batchPaths = untagged.slice(i, i + BATCH_SIZE);
|
const batchPaths = untagged.slice(i, i + BATCH_SIZE);
|
||||||
|
|
@ -217,14 +221,16 @@ export async function processUntaggedNotes(): Promise<void> {
|
||||||
console.log(`[NoteTagging] Batch ${batchNumber}/${totalBatches} complete, ${result.filesEdited.size} files tagged`);
|
console.log(`[NoteTagging] Batch ${batchNumber}/${totalBatches} complete, ${result.filesEdited.size} files tagged`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
|
failedBatches++;
|
||||||
|
const errorDetails = getErrorDetails(error);
|
||||||
console.error(`[NoteTagging] Error processing batch ${batchNumber}:`, error);
|
console.error(`[NoteTagging] Error processing batch ${batchNumber}:`, error);
|
||||||
await serviceLogger.log({
|
await serviceLogger.log({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
service: run.service,
|
service: run.service,
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `Error processing batch ${batchNumber}`,
|
message: `Note tagging batch ${batchNumber}/${totalBatches} failed`,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: errorDetails,
|
||||||
context: { batchNumber },
|
context: { batchNumber },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -238,12 +244,15 @@ export async function processUntaggedNotes(): Promise<void> {
|
||||||
service: run.service,
|
service: run.service,
|
||||||
runId: run.runId,
|
runId: run.runId,
|
||||||
level: hadError ? 'error' : 'info',
|
level: hadError ? 'error' : 'info',
|
||||||
message: `Note tagging complete: ${totalEdited} notes tagged`,
|
message: hadError
|
||||||
|
? `Note tagging finished with errors: ${totalEdited} notes tagged`
|
||||||
|
: `Note tagging complete: ${totalEdited} notes tagged`,
|
||||||
durationMs: Date.now() - run.startedAt,
|
durationMs: Date.now() - run.startedAt,
|
||||||
outcome: hadError ? 'error' : 'ok',
|
outcome: hadError ? 'error' : 'ok',
|
||||||
summary: {
|
summary: {
|
||||||
totalNotes: untagged.length,
|
totalNotes: untagged.length,
|
||||||
notesTagged: totalEdited,
|
notesTagged: totalEdited,
|
||||||
|
failedBatches,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue