fix: add type guards and null checks for UUID handling in snip functionality

- Add UUID type import and type guard in force-snip command
- Add null/undefined checks in snipCompact, snipProjection, and sessionStorage
- Improve optional chaining in SnipTool for safer property access
- Prevent potential runtime errors from invalid UUID values

Co-Authored-By: claude-sonnet-4-6 <noreply@anthropic.com>
This commit is contained in:
DoSun 2026-05-15 14:25:42 +08:00
parent de83394e8a
commit ecb0df50d3
5 changed files with 14 additions and 10 deletions

View File

@ -79,13 +79,11 @@ Guidelines:
}, },
async call(input: SnipInput) { async call(input: SnipInput) {
// Snip implementation is handled by the query engine's projection system. const ids = input?.message_ids ?? []
// The tool call itself records the intent; the query engine intercepts
// snip tool results and adjusts its message projection accordingly.
return { return {
data: { data: {
snipped_count: input.message_ids.length, snipped_count: ids.length,
summary: input.reason ?? `Snipped ${input.message_ids.length} messages`, summary: input?.reason ?? `Snipped ${ids.length} messages`,
}, },
} }
}, },

View File

@ -1,3 +1,4 @@
import type { UUID } from 'crypto'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import type { Command, LocalCommandCall } from '../types/command.js' import type { Command, LocalCommandCall } from '../types/command.js'
import type { Message } from '../types/message.js' import type { Message } from '../types/message.js'
@ -25,7 +26,7 @@ const call: LocalCommandCall = async (_args, context) => {
// Collect UUIDs of every message that will be snipped (everything currently // Collect UUIDs of every message that will be snipped (everything currently
// in the conversation). The next call to `snipCompactIfNeeded` will honour // in the conversation). The next call to `snipCompactIfNeeded` will honour
// the boundary and strip these from the model-facing view. // the boundary and strip these from the model-facing view.
const removedUuids = messages.map(m => m.uuid) const removedUuids = messages.map(m => m.uuid).filter((u): u is UUID => !!u)
const boundaryMessage: Message = { const boundaryMessage: Message = {
type: 'system', type: 'system',

View File

@ -126,12 +126,15 @@ export function snipCompactIfNeeded(
} }
// Filter out messages whose UUIDs are listed in removedUuids // Filter out messages whose UUIDs are listed in removedUuids
const removedSet = new Set(removedUuids) // Guard: filter out any falsy entries (e.g. messages that lacked a uuid)
const removedSet = new Set<string>(
removedUuids.filter((u): u is string => typeof u === 'string'),
)
const kept: Message[] = [] const kept: Message[] = []
let tokensFreed = 0 let tokensFreed = 0
for (const msg of messages) { for (const msg of messages) {
if (removedSet.has(msg.uuid)) { if (msg.uuid && removedSet.has(msg.uuid)) {
tokensFreed += estimateMessageTokens(msg) tokensFreed += estimateMessageTokens(msg)
continue continue
} }

View File

@ -46,7 +46,7 @@ export function projectSnippedView(messages: Message[]): Message[] {
| undefined | undefined
if (meta?.removedUuids) { if (meta?.removedUuids) {
for (const uuid of meta.removedUuids) { for (const uuid of meta.removedUuids) {
removedSet.add(uuid) if (typeof uuid === 'string') removedSet.add(uuid)
} }
} }
} }

View File

@ -1996,7 +1996,9 @@ function applySnipRemovals(messages: Map<UUID, TranscriptMessage>): void {
for (const entry of messages.values()) { for (const entry of messages.values()) {
const removedUuids = (entry as WithSnipMeta).snipMetadata?.removedUuids const removedUuids = (entry as WithSnipMeta).snipMetadata?.removedUuids
if (!removedUuids) continue if (!removedUuids) continue
for (const uuid of removedUuids) toDelete.add(uuid) for (const uuid of removedUuids) {
if (uuid) toDelete.add(uuid)
}
} }
if (toDelete.size === 0) return if (toDelete.size === 0) return