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.
This commit is contained in:
Brooklyn Nicholson 2026-07-29 18:47:15 -05:00
parent 4ae5efa3f4
commit c0364fa02e
8 changed files with 72 additions and 5 deletions

View file

@ -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<Parameters<typeof part>[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')
})
})

View file

@ -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<string, unknown>): 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,

View file

@ -2349,6 +2349,7 @@ export const ar = defineLocale({
statusError: 'خطأ',
statusRecovered: 'تم الاسترداد',
statusDone: 'تم',
memoryWriteNoted: 'تم تسجيل كتابة الذاكرة',
actions: {
read: 'قراءة',
reading: 'جار القراءة',

View file

@ -2758,6 +2758,7 @@ export const en: Translations = {
statusError: 'Error',
statusRecovered: 'Recovered',
statusDone: 'Done',
memoryWriteNoted: 'Memory write noted',
actions: {
read: 'Read',
reading: 'Reading',

View file

@ -2602,6 +2602,7 @@ export const ja = defineLocale({
statusError: 'エラー',
statusRecovered: '回復しました',
statusDone: '完了',
memoryWriteNoted: 'メモリへの書き込みを記録',
actions: {
read: '読み取り完了',
reading: '読み取り中',

View file

@ -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

View file

@ -2521,6 +2521,7 @@ export const zhHant = defineLocale({
statusError: '錯誤',
statusRecovered: '已復原',
statusDone: '完成',
memoryWriteNoted: '已記下記憶寫入',
actions: {
read: '已讀取',
reading: '正在讀取',

View file

@ -2933,6 +2933,7 @@ export const zh: Translations = {
statusError: '错误',
statusRecovered: '已恢复',
statusDone: '完成',
memoryWriteNoted: '已记下记忆写入',
actions: {
read: '已读取',
reading: '正在读取',