diff --git a/src/browser/components/AutomationModal/AutomationModal.tsx b/src/browser/components/AutomationModal/AutomationModal.tsx index b7c9df8f07..b13b31ba55 100644 --- a/src/browser/components/AutomationModal/AutomationModal.tsx +++ b/src/browser/components/AutomationModal/AutomationModal.tsx @@ -34,6 +34,12 @@ import { workflowScheduleIntervalMinutesToMs, } from "@/browser/utils/workflowScheduleIntervalMinutes"; +// Fallback messages shown when the save/remove RPC fails without a specific +// error string. Deduplicated so the wording stays consistent across the +// pre-check, RPC-result, and thrown-error paths below. +const SAVE_AUTOMATION_ERROR_MESSAGE = "Failed to save automation."; +const REMOVE_AUTOMATION_ERROR_MESSAGE = "Failed to remove automation."; + interface AutomationModalProps { open: boolean; projectPath: string; @@ -308,7 +314,7 @@ export function AutomationModal(props: AutomationModalProps) { schedule: workspaceSchedule, }); if (!result.success) { - setSaveError(result.error ?? "Failed to save automation."); + setSaveError(result.error ?? SAVE_AUTOMATION_ERROR_MESSAGE); return false; } } else { @@ -327,7 +333,7 @@ export function AutomationModal(props: AutomationModalProps) { }, }); if (!result.success) { - setSaveError(result.error ?? "Failed to save automation."); + setSaveError(result.error ?? SAVE_AUTOMATION_ERROR_MESSAGE); return false; } } @@ -335,7 +341,7 @@ export function AutomationModal(props: AutomationModalProps) { await refreshProjects(); return true; } catch (error) { - setSaveError(getErrorMessage(error) || "Failed to save automation."); + setSaveError(getErrorMessage(error) || SAVE_AUTOMATION_ERROR_MESSAGE); return false; } finally { setIsSaving(false); @@ -388,7 +394,7 @@ export function AutomationModal(props: AutomationModalProps) { scheduleId: props.projectWorkflowSchedule.id, }); if (!result.success) { - throw new Error(result.error ?? "Failed to remove automation."); + throw new Error(result.error ?? REMOVE_AUTOMATION_ERROR_MESSAGE); } } else if (isLegacyWorkspaceSchedule) { const result = await api.workspace.setWorkflowSchedule({ @@ -396,13 +402,13 @@ export function AutomationModal(props: AutomationModalProps) { schedule: null, }); if (!result.success) { - throw new Error(result.error ?? "Failed to remove automation."); + throw new Error(result.error ?? REMOVE_AUTOMATION_ERROR_MESSAGE); } } await refreshProjects(); props.onOpenChange(false); } catch (error) { - setSaveError(getErrorMessage(error) || "Failed to remove automation."); + setSaveError(getErrorMessage(error) || REMOVE_AUTOMATION_ERROR_MESSAGE); } finally { setIsSaving(false); } diff --git a/src/browser/features/Tools/WorkflowRunToolCall.tsx b/src/browser/features/Tools/WorkflowRunToolCall.tsx index 8d2c8bbb90..af84af0a7c 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.tsx @@ -1077,10 +1077,6 @@ export const WorkflowRunToolCall: React.FC = ({ resetKey: runId, }); - const toggleWorkflowExpanded = () => { - toggleExpanded(); - }; - const [actionError, setActionError] = useState(null); const [promotedDefinition, setPromotedDefinition] = useState( null @@ -1433,7 +1429,7 @@ export const WorkflowRunToolCall: React.FC = ({ return ( - + diff --git a/src/node/services/analytics/etl.ts b/src/node/services/analytics/etl.ts index 41adc6815f..0b3277fe96 100644 --- a/src/node/services/analytics/etl.ts +++ b/src/node/services/analytics/etl.ts @@ -9,9 +9,12 @@ import { getErrorMessage } from "@/common/utils/errors"; import { createDisplayUsage } from "@/common/utils/tokens/displayUsage"; import { log } from "@/node/services/log"; import { toUtcDateString } from "@/node/services/analytics/dateUtils"; -import { CHAT_ARCHIVE_FILE_NAME } from "@/common/constants/paths"; +import { CHAT_FILE_NAME, CHAT_ARCHIVE_FILE_NAME } from "@/common/constants/paths"; -export const CHAT_FILE_NAME = "chat.jsonl"; +// Re-export the canonical chat history filename (defined in constants/paths.ts) +// so existing analytics consumers (workspaceDiscovery, tests) can keep importing +// it from this module without duplicating the "chat.jsonl" literal. +export { CHAT_FILE_NAME }; /** * Sealed pre-boundary history rotates from chat.jsonl into chat-archive.jsonl diff --git a/src/node/services/memoryConsolidationService.ts b/src/node/services/memoryConsolidationService.ts index a119ac2304..f8d2980839 100644 --- a/src/node/services/memoryConsolidationService.ts +++ b/src/node/services/memoryConsolidationService.ts @@ -177,13 +177,22 @@ function findNewestWorkspaceRecord( ); } +/** + * Effective ordering timestamp for a harvest record: when it completed, or when + * it started if still pending. findNewestHarvestRecord and pruneHarvestRecords + * must rank records the same way, so both derive recency from this single key. + */ +function harvestRecordTime(record: MemoryHarvestRecord): number { + return record.completedAt ?? record.startedAt; +} + function findNewestHarvestRecord( records: Record | undefined ): MemoryHarvestRecord | null { if (records === undefined) return null; return Object.values(records).reduce((latest, record) => { - const recordTime = record.completedAt ?? record.startedAt; - const latestTime = latest === null ? -1 : (latest.completedAt ?? latest.startedAt); + const recordTime = harvestRecordTime(record); + const latestTime = latest === null ? -1 : harvestRecordTime(latest); return recordTime > latestTime ? record : latest; }, null); } @@ -231,11 +240,9 @@ function parseHarvestRecords(value: unknown): Record): void { - const ranked = Object.entries(records).sort(([, left], [, right]) => { - const leftTime = left.completedAt ?? left.startedAt; - const rightTime = right.completedAt ?? right.startedAt; - return rightTime - leftTime; - }); + const ranked = Object.entries(records).sort( + ([, left], [, right]) => harvestRecordTime(right) - harvestRecordTime(left) + ); for (const [boundaryKey] of ranked.slice(HARVEST_RECORD_RETENTION)) { delete records[boundaryKey]; } diff --git a/src/node/services/workflows/WorkflowRunner.ts b/src/node/services/workflows/WorkflowRunner.ts index a5ac3b7b67..de8c29089f 100644 --- a/src/node/services/workflows/WorkflowRunner.ts +++ b/src/node/services/workflows/WorkflowRunner.ts @@ -15,6 +15,7 @@ import assert from "@/common/utils/assert"; import { getErrorMessage } from "@/common/utils/errors"; import { validateJsonSchemaSubset } from "@/common/utils/jsonSchemaSubset"; import type { IJSRuntime, IJSRuntimeFactory } from "@/node/services/ptc/runtime"; +import { isWorkflowRunTaskId } from "@/node/services/tools/taskId"; import { AsyncMutex } from "@/node/utils/concurrency/asyncMutex"; import { AsyncSemaphore } from "@/node/utils/concurrency/asyncSemaphore"; import type { ResolvedWorkflowAction, WorkflowActionRegistry } from "./WorkflowActionRegistry"; @@ -999,7 +1000,7 @@ export class WorkflowRunner { const run = await this.runStore.getRun(runId); const driftedStep = run.steps.find( (step) => - step.stepId === spec.id && step.inputHash !== inputHash && step.taskId?.startsWith("wfr_") + step.stepId === spec.id && step.inputHash !== inputHash && isWorkflowRunTaskId(step.taskId) ); if (driftedStep != null) { throw new Error(