fix: address ds-review-bot v7 findings on the merged image-input head

- gate model selection on steering-placement image carriers from enqueue
  until their steering/message event publishes; release the gate when an
  admission ends idle without publication (both behaviorally asserted)
- reject session.updateQueue edits carrying non-text blocks at the RPC
  boundary (queue edits cannot bypass image admission)
- extend the durable-directory walk past a first-created DSH_HOME to the
  deepest pre-existing ancestor
- strip Windows-style separators from attachment display names on POSIX
- verify attachment reads with a header-only probe (digest already proves
  the bytes decoded fully at admission); document the read path
- make SessionInputShell.addImages refusal observable and keep workspace
  transfers/composer intake from leaking refused drafts
- own ONE recursive image walk (dsh-llm contentHasImage) across apiproxy,
  pi-ai, compact-basic, and the DeepSeek text-only assertion
- drop the redundant canonical-base64 regex and the no-op role read
- move AttachmentId/AttachmentError out of types.ts (brand.ts/error.ts);
  document why AttachmentError does not extend HarnessError
- document the hard attachments inject in both consumer READMEs
This commit is contained in:
creatixchu
2026-07-30 14:34:08 +08:00
parent 97cf33b7e0
commit 0d1250f743
37 changed files with 335 additions and 140 deletions
+3 -6
View File
@@ -33,7 +33,8 @@ import type {
import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout'
import { resolveProfiles } from './config.ts'
import type { PiAiProviderProfile, ResolvedPiAiProviderProfile } from './config.ts'
import { contentHasImage, toPiContext } from './context.ts'
import { contentHasImage } from '@deepseek-ai/dsh-llm'
import { toPiContext } from './context.ts'
import { toStreamChunks } from './stream.ts'
/** Constructor options for {@link PiAiAdapter}. */
@@ -189,11 +190,7 @@ export class PiAiAdapter extends LlmAdapter {
using watchdog = idleWatchdog(upstream, streamIdleTimeoutMs, 'LLM_STREAM_IDLE_TIMEOUT')
try {
const containsImage = options.messages.some((message) => {
// The discriminant is part of same-process message validity and is read before content.
void message.role
return contentHasImage(message.content)
})
const containsImage = options.messages.some(message => contentHasImage(message.content))
if (containsImage && !model.input.includes('image')) {
throw new LlmError(`pi-ai model "${model.id}" does not support image input`, 'UNSUPPORTED_CONTENT')
}
+1 -10
View File
@@ -4,7 +4,7 @@
* @module dsh-llm-pi-ai/context
*/
import { CallId, LlmError } from '@deepseek-ai/dsh-llm'
import { CallId, contentHasImage, LlmError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment'
import type { Context as PiContext, ImageContent, Message as PiMessage, TextContent, Tool as PiTool } from '@earendil-works/pi-ai'
@@ -18,15 +18,6 @@ function flattenText(message: Message): string {
.join('')
}
/**
* Return whether content contains an image, including nested tool results.
* @param blocks - content to inspect recursively.
* @returns whether any nested block is an image.
*/
export function contentHasImage(blocks: readonly ContentBlock[]): boolean {
return blocks.some(block => block.type === 'image'
|| (block.type === 'tool-result' && contentHasImage(block.content)))
}
/** Flatten text recursively inside one tool result. */
function toolResultText(blocks: readonly ContentBlock[]): string {