From 27bf4a7f17f743d0efc04dfe97c44dae67527ba5 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:07:51 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20extract=20harves?= =?UTF-8?q?tRecordTime=20ordering=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deduplicate the `completedAt ?? startedAt` harvest-record ordering key in src/node/services/memoryConsolidationService.ts. After #3558 ("add memory harvest pipeline"), the same effective-time expression was computed inline four times across two functions that must rank records identically: - findNewestHarvestRecord picks the record with the max completedAt/startedAt. - pruneHarvestRecords sorts descending by the same key to keep the newest 20. Both now derive recency from one `harvestRecordTime(record)` helper, documenting that they must agree on "newest" so the format can't drift between call sites. Behavior-preserving: the helper returns the identical value, the reduce/sort comparisons are unchanged, and normalizeHarvestRecord's distinct `completedAt ?? Date.now()` (finalizing a stale record) is intentionally left inline. Verified with `bun test memoryConsolidationService.test.ts` (pass) plus eslint + tsc. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-8` • Thinking: `xhigh` • Cost: `n/a`_ --- .../services/memoryConsolidationService.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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]; } From bc54f3eadef9ebc92dc3ff20f1d47c44c021bb38 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:42:40 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20dedupe=20CHAT=5F?= =?UTF-8?q?FILE=5FNAME=20literal=20in=20analytics=20etl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit etl.ts redefined `export const CHAT_FILE_NAME = "chat.jsonl"` even though #3541 added the canonical constant in src/common/constants/paths.ts (and etl.ts already imports its sibling CHAT_ARCHIVE_FILE_NAME from there). Re-export the canonical constant instead so the "chat.jsonl" literal lives in one place; analytics consumers (workspaceDiscovery, tests) keep importing CHAT_FILE_NAME from ./etl unchanged. Behavior-preserving: identical value. --- src/node/services/analytics/etl.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From 9b10402224c0e3f947836150a1443c412b832b1f Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:07:07 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20use=20isWorkflow?= =?UTF-8?q?RunTaskId=20helper=20for=20drift=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3565 reintroduced a hardcoded startsWith("wfr_") in WorkflowRunner's nested-step drift detection. taskId.ts already exports the canonical WORKFLOW_RUN_TASK_ID_PREFIX and isWorkflowRunTaskId() predicate as the single source of truth for that prefix. Use the helper instead of the duplicated literal; behavior is identical (both are falsy for a missing taskId inside the find() predicate). --- src/node/services/workflows/WorkflowRunner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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( From 46a75d6b35ac66390f56f5d720d08426d11471ed Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:43:13 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20dedupe=20automat?= =?UTF-8?q?ion=20modal=20error=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the repeated "Failed to save automation." / "Failed to remove automation." fallback strings in AutomationModal.tsx into two module-level constants. Behavior-preserving: each call site now references an identical string constant. --- .../AutomationModal/AutomationModal.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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); } From 797ab846cb27ac5dc9f48d6739b14d2869884b8b Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:49:52 +0000 Subject: [PATCH 5/5] refactor: drop redundant toggleWorkflowExpanded wrapper After #3571 refactored workflow run expansion onto useAutoCollapsingToolExpansion, toggleWorkflowExpanded became a pure pass-through to the already-stable toggleExpanded callback. Inline it at the single ToolHeader onClick call site. Behavior-preserving: both are () => void and the wrapper added no logic. --- src/browser/features/Tools/WorkflowRunToolCall.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 ( - +