fix: preserve tool details and prevent repeated history growth

Show bounded operation summaries for Continue, DeepSeek Harness and direct-model tools, retaining input details across incremental updates. Use one canonical workspace path for DeepSeek Host startup and ACP sessions so equivalent Windows paths reuse a working Runtime.

Compare persisted subagent tool fields by value, release terminal task caches, and avoid rebuilding caches for duplicate remote events. Upgrade to schema 35 to recompact repeated schema-34 tool blocks while preserving events and provenance. Include regression coverage, validation records and temporary-resource test exclusions.

Validation: 4155 tests passed, 66 skipped; typecheck and lint passed. Full desktop real-model validation covered all three tool-summary paths and both Windows path spellings. Storage validation evidence includes local and remote tool workloads and real database upgrade checks.

Release note: Tool records now show paths, commands and search details across runtimes. Fix DeepSeek Windows workspace failures and repeated subagent history growth; schema 35 reclaims redundant history data while preserving execution records, and older clients cannot reopen the upgraded database.
This commit is contained in:
mesalogo
2026-09-14 00:34:51 +08:00
parent c71da4b6ae
commit 0d7ba55a50
29 changed files with 667 additions and 97 deletions
@@ -4,9 +4,28 @@ import type { DatabaseSync } from 'node:sqlite'
import {
assistantIdSchema,
conversationMessageBlockSchema,
type ConversationMessageBlock
conversationToolActivitySchema,
type ConversationMessageBlock,
type ConversationToolActivity
} from '../../shared/assistant-contracts'
export const SUBAGENT_PROGRESS_STORAGE_SCHEMA_VERSION = 35
const toolFields = Object.keys(conversationToolActivitySchema.shape) as
Array<keyof ConversationToolActivity>
function sameProgressBlock(
previous: ConversationMessageBlock,
next: ConversationMessageBlock
): boolean {
if (previous.type === 'tool' && next.type === 'tool') {
// JSON omits undefined optional fields. Compare their values, not key presence.
return previous.id === next.id &&
toolFields.every((field) => previous.tool[field] === next.tool[field])
}
return isDeepStrictEqual(previous, next)
}
export const subagentProgressUpdateSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('append'),
@@ -89,7 +108,7 @@ export function diffSubagentProgress(
type: 'append', id: block.id, blockType: block.type, delta
})
}
} else if (!prior || !isDeepStrictEqual(prior, block)) {
} else if (!prior || !sameProgressBlock(prior, block)) {
updates.push({ type: 'upsert', block })
}
}
@@ -114,12 +133,15 @@ export function compactSubagentPayload(
progress: ReadonlyMap<string, ConversationMessageBlock[]>
): unknown {
const event = asSubagent(payload)
if (!event || !Array.isArray(event.progress)) return payload
if (!event || (
!Array.isArray(event.progress) && !Array.isArray(event.progressUpdates)
)) return payload
const previous = progress.get(event.childTaskId) ?? []
const compact = { ...event }
delete compact.progress
compact.progressUpdates = diffSubagentProgress(
progress.get(event.childTaskId) ?? [],
event.progress
previous,
applySubagentProgress(previous, event) ?? []
)
return compact
}
@@ -158,7 +180,17 @@ export class SubagentProgressStorage {
}
inserted(taskId: string, kind: string, id: number, payloadJson: string): void {
if (kind !== 'subagent') return
if (kind !== 'subagent') {
if (
kind === 'done' || kind === 'error' ||
(kind === 'status' && [
'completed', 'failed', 'cancelled', 'interrupted'
].includes((JSON.parse(payloadJson) as { status?: string }).status ?? ''))
) {
this.tasks.delete(taskId)
}
return
}
const task = this.tasks.get(taskId)
if (!task) return
restoreSubagentPayload(JSON.parse(payloadJson), task.blocks)