From c0364fa02eadf26abaec99c42f4b4c142b85ff75 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 29 Jul 2026 18:47:15 -0500 Subject: [PATCH] fix(desktop): treat landed memory writes as success, not warnings Trust explicit success over a stale isError envelope so real saves stop painting amber. Over-budget refusals keep a soft warning with "Memory write noted" instead of crowing "Saved", and entry_count labels as entries. --- .../assistant-ui/tool/fallback-model.test.ts | 35 +++++++++++++++++++ .../assistant-ui/tool/fallback-model/index.ts | 35 ++++++++++++++++--- apps/desktop/src/i18n/ar.ts | 1 + apps/desktop/src/i18n/en.ts | 1 + apps/desktop/src/i18n/ja.ts | 1 + apps/desktop/src/i18n/types.ts | 2 ++ apps/desktop/src/i18n/zh-hant.ts | 1 + apps/desktop/src/i18n/zh.ts | 1 + 8 files changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts b/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts index 19a8cd61447..3ad0e174e1c 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts @@ -415,3 +415,38 @@ describe('countDiffLineStats', () => { expect(countDiffLineStats(`--- a/x\n+++ b/x\n@@\n-old\n+new\n context\n+another`)).toEqual({ added: 2, removed: 1 }) }) }) + +describe('buildToolView memory status', () => { + const memory = (overrides: Partial[0]> = {}) => + buildToolView(part({ toolName: 'memory', ...overrides }), '') + + it('treats an explicit success payload as success even with isError', () => { + const view = memory({ + isError: true, + result: { + success: true, + entry_count: 13, + message: 'Applied 1 operation(s).', + duration_s: 0.003 + } + }) + + expect(view.status).toBe('success') + expect(view.title).toBe('Saved to memory') + expect(view.countLabel).toBe('13 entries') + expect(view.subtitle).toBe('Applied 1 operation(s).') + }) + + it('uses soft warning copy for over-budget refusals, not "Saved"', () => { + const view = memory({ + result: { + success: false, + error: 'Memory is full (2,200/2,200). Consolidate before adding more.' + } + }) + + expect(view.status).toBe('warning') + expect(view.title).toBe('Memory write noted') + expect(view.subtitle).toContain('Memory is full') + }) +}) diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts b/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts index 5a7a5066c62..52e6a44a8d1 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback-model/index.ts @@ -516,6 +516,16 @@ function toolResultCount( } } + // Memory success payloads put the live total on `entry_count` — keep the noun + // as entry/entries instead of falling through the generic `*_count` path. + if (part.toolName === 'memory') { + const entryTotal = countFromUnknown(resultRecord.entry_count) + + if (entryTotal !== null) { + return countMetric(entryTotal, 'entry') + } + } + const directCount = countFromRecord(resultRecord, fallbackNounByTool) if (directCount !== null) { @@ -699,14 +709,20 @@ function toolStatus(part: ToolPart, resultRecord: Record): Tool return 'running' } + // Explicit success wins over isError / nested-error heuristics. Memory writes + // return `{ success: true }` when the batch landed; a stale outer `isError` + // envelope must not paint a real save amber. + if (resultRecord.success === true || resultRecord.ok === true) { + return 'success' + } + if (!toolErrorText(part, resultRecord)) { return 'success' } // A rejected memory write is a budget negotiation, not a failure: the store - // refuses an over-limit batch and the agent immediately retries a smaller - // one. Painting the row destructive-red puts an alarm next to routine - // bookkeeping the user never has to act on. Amber says "noted" instead. + // refuses an over-limit batch and the agent retries smaller. Soft warning — + // never destructive-red beside routine bookkeeping. return part.toolName === 'memory' ? 'warning' : 'error' } @@ -1395,8 +1411,17 @@ export function buildToolView(part: ToolPart, inlineDiff: string): ToolView { const resultRecord = parseMaybeObject(part.result) const meta = toolMeta(part.toolName) const status = toolStatus(part, resultRecord) - const error = toolErrorText(part, resultRecord) - const baseTitle = part.result === undefined ? meta.pending : meta.done + // Skip residual error-heuristic text once status is success (stale isError + // envelope over a landed memory write would otherwise foul the subtitle). + const error = status === 'success' ? '' : toolErrorText(part, resultRecord) + // Over-budget memory refusals stay amber — don't claim "Saved". + const memoryMissed = part.toolName === 'memory' && part.result !== undefined && status !== 'success' + const baseTitle = + part.result === undefined + ? meta.pending + : memoryMissed + ? translateNow('assistant.tool.memoryWriteNoted') + : meta.done const titleParts = dynamicTitle( part, diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts index c72898a9cea..48866637b0d 100644 --- a/apps/desktop/src/i18n/ar.ts +++ b/apps/desktop/src/i18n/ar.ts @@ -2349,6 +2349,7 @@ export const ar = defineLocale({ statusError: 'خطأ', statusRecovered: 'تم الاسترداد', statusDone: 'تم', + memoryWriteNoted: 'تم تسجيل كتابة الذاكرة', actions: { read: 'قراءة', reading: 'جار القراءة', diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index d55c494e070..8ada8b48b31 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -2758,6 +2758,7 @@ export const en: Translations = { statusError: 'Error', statusRecovered: 'Recovered', statusDone: 'Done', + memoryWriteNoted: 'Memory write noted', actions: { read: 'Read', reading: 'Reading', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index ad8f206abe2..84b9cf95201 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -2602,6 +2602,7 @@ export const ja = defineLocale({ statusError: 'エラー', statusRecovered: '回復しました', statusDone: '完了', + memoryWriteNoted: 'メモリへの書き込みを記録', actions: { read: '読み取り完了', reading: '読み取り中', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 8ccc8a034ca..3ec175fd002 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -2354,6 +2354,8 @@ export interface Translations { statusError: string statusRecovered: string statusDone: string + /** Over-budget / rejected memory write title — not "Saved to memory". */ + memoryWriteNoted: string actions: { read: string reading: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 5343d395724..e27b8284fd5 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -2521,6 +2521,7 @@ export const zhHant = defineLocale({ statusError: '錯誤', statusRecovered: '已復原', statusDone: '完成', + memoryWriteNoted: '已記下記憶寫入', actions: { read: '已讀取', reading: '正在讀取', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 86049412b4c..1797214d098 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -2933,6 +2933,7 @@ export const zh: Translations = { statusError: '错误', statusRecovered: '已恢复', statusDone: '完成', + memoryWriteNoted: '已记下记忆写入', actions: { read: '已读取', reading: '正在读取',