Merge remote-tracking branch 'origin/master' into feat/read-image-context

# Conflicts:
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	packages/support/llm-replay/src/index.ts
#	packages/support/llm-replay/tests/llm-replay.spec.ts
This commit is contained in:
creatixchu
2026-08-10 20:25:25 +08:00
43 changed files with 1032 additions and 130 deletions

View File

@@ -15,6 +15,8 @@ import { FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import { copyFileDaclWin32, replaceFileWin32 } from './win32.ts'
const BINARY_SAMPLE_BYTES = 8192
// Bound one non-abortable FileHandle.read so cancellation is observed between chunks.
const DIFF_BASIS_READ_CHUNK_BYTES = 64 * 1024
function isENOENT(error: unknown): boolean {
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
@@ -74,9 +76,8 @@ function versionOf(info: BigIntStats): FsVersion {
}
/**
* Test hook: lets specs pin the atomic-write temp names (to prove
* exclusive-open behavior without a name race) and observe the staged temp
* file before it is renamed over the target.
* Test hook: lets specs pin the atomic-write temp names (to prove exclusive-open behavior without
* a name race), override native boundaries, and observe the staged temp file before publication.
*/
export interface FsIoInternals {
/** Override the host platform for native-publication unit coverage. */
@@ -680,21 +681,67 @@ export async function readForEdit(
}
/**
* Best-effort overwrite diff basis. Binary or invalid UTF-8 returns `null` so the write still
* succeeds and presentation falls back to a whole-file diff.
* @param absolutePath - the file to read (typically a target key); it must exist.
* @param signal - aborts the read (`FS_ABORTED`).
* @returns the LF-normalized text, or null for a binary or non-UTF-8 file.
* Best-effort overwrite diff basis. Binary, invalid UTF-8, a file at/above the byte limit,
* or a file deleted/made unreadable after the caller's preflight returns `null` so the write
* still succeeds and presentation falls back to a whole-file diff. The bound is enforced on
* the opened descriptor rather than a prior path stat, so concurrent external replacement or
* size changes cannot make this helper buffer more than `maxBytes`.
* @param absolutePath - the file to read (typically a target key).
* @param maxBytes - exclusive upper bound for bytes held as the contextual-diff basis.
* @param signal - aborts the read (`FS_ABORTED`); cancellation propagates, unlike I/O failure.
* @returns the LF-normalized text, or null for a non-regular, at/above-limit, binary, non-UTF-8,
* descriptor-size-changed, or unreadable file.
*/
export async function readTextForDiff(absolutePath: string, signal?: AbortSignal): Promise<string | null> {
const buffer = await readFileAbortable(absolutePath, 'read', signal)
if (buffer.includes(0)) return null
export async function readTextForDiff(
absolutePath: string,
maxBytes: number,
signal?: AbortSignal,
): Promise<string | null> {
throwIfAborted(signal, 'read')
try {
return normalizeLineEndings(new TextDecoder('utf-8', { fatal: true }).decode(buffer))
const handle = await open(absolutePath, 'r')
let buffer: Buffer
let total = 0
let openedSize = 0
try {
throwIfAborted(signal, 'read')
const info = await handle.stat()
throwIfAborted(signal, 'read')
if (!info.isFile()) return null
if (info.size >= maxBytes) return null
openedSize = info.size
// One extra byte detects growth after stat without retaining per-read backing buffers.
buffer = Buffer.allocUnsafe(openedSize + 1)
while (total < buffer.length) {
throwIfAborted(signal, 'read')
const length = Math.min(buffer.length - total, DIFF_BASIS_READ_CHUNK_BYTES)
const { bytesRead } = await handle.read(buffer, total, length, null)
if (bytesRead === 0) break
total += bytesRead
}
} finally {
await handle.close()
}
throwIfAborted(signal, 'read')
if (total !== openedSize) return null
const basis = buffer.subarray(0, total)
if (basis.includes(0)) return null
try {
return normalizeLineEndings(new TextDecoder('utf-8', { fatal: true }).decode(basis))
} catch (error: unknown) {
/* v8 ignore next 2 -- TextDecoder({fatal}) only throws TypeError on invalid bytes;
* any other throw is an unreachable runtime fault. */
if (!(error instanceof TypeError)) throw error
return null
}
} catch (error: unknown) {
/* v8 ignore next 2 -- TextDecoder({fatal}) only throws TypeError on invalid bytes; any other throw is an unreachable runtime fault. */
if (!(error instanceof TypeError)) throw error
return null
// Cancellation is the caller's intent and still propagates.
if (error instanceof FsError) throw error
// A descriptor-phase errno — deleted or made unreadable after the caller's
// preflight, or a faulted read — costs only the optional basis: a committed
// write must not fail for a presentation-only pre-read.
if (error instanceof Error && 'code' in error) return null
throw error
}
}

View File

@@ -5,6 +5,7 @@
*/
import { Context } from 'cordis'
import { constants as bufferConstants } from 'node:buffer'
import { isAbsolute, relative, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
import z from 'schemastery'
@@ -40,9 +41,19 @@ import type { FsIoInternals } from './fsio.ts'
export interface Config {
/** Base directory for relative paths. Defaults to `process.cwd()`. */
cwd?: string
/**
* Exclusive UTF-8 byte limit on each overwrite-diff side, capped by the
* runtime's safe allocation/decode maximum. Defaults to 10 MiB.
*/
diffBasisMaxBytes?: number
}
type ResolvedConfig = Required<Config>
const DEFAULT_DIFF_BASIS_MAX_BYTES = 10 * 1024 * 1024
const MAX_DIFF_BASIS_BYTES = Math.min(
bufferConstants.MAX_LENGTH,
bufferConstants.MAX_STRING_LENGTH,
)
/**
* The host-filesystem backend. Reads resolve relative paths from {@link Config.cwd}
@@ -53,11 +64,12 @@ type ResolvedConfig = Required<Config>
export class LocalFileSystem extends FileSystem {
static Config: z<Config> = z.object({
cwd: z.string().default(process.cwd()),
diffBasisMaxBytes: z.number().default(DEFAULT_DIFF_BASIS_MAX_BYTES),
})
/** Validated config (schemastery applied the defaults before construction). */
readonly config: ResolvedConfig
/** Test hook forwarded to fsio (force streaming path, pin temp names). */
/** Test hook forwarded to fsio for atomic-publication boundaries. */
internals: FsIoInternals = {}
/** Per-targetKey tail promise: serializes mutating ops so the read→guard→write
* window can't interleave, making concurrent writes/edits deterministically
@@ -66,7 +78,13 @@ export class LocalFileSystem extends FileSystem {
constructor(ctx: Context, config: Config) {
super(ctx)
this.config = config as ResolvedConfig
const resolved = config as ResolvedConfig
if (!Number.isSafeInteger(resolved.diffBasisMaxBytes)
|| resolved.diffBasisMaxBytes <= 0
|| resolved.diffBasisMaxBytes > MAX_DIFF_BASIS_BYTES) {
throw new Error(`fs-local: diffBasisMaxBytes must be a positive safe integer no greater than ${MAX_DIFF_BASIS_BYTES}`)
}
this.config = resolved
}
/** Run `op` with exclusive access to `targetKey` (FIFO per key). */
@@ -169,9 +187,16 @@ export class LocalFileSystem extends FileSystem {
}
// No expectation means an unconditional but still atomic write.
// Preserve prior text for contextual diffs; null falls back to a whole-file diff.
// TODO(overwrite-diff-bound): cap this UI-only pre-read for large files.
const before = existing ? await readTextForDiff(target.targetKey, signal) : null
// Capture an optional contextual-diff basis before the write. The bounded
// reader checks the opened file itself, so an external replacement after
// `probe()` cannot turn this best-effort presentation read into an
// unbounded allocation. Either side at/above the configured limit yields
// `before: null`; consumers retain their whole-file fallback.
const diffable = existing !== null
&& Buffer.byteLength(content, 'utf8') < this.config.diffBasisMaxBytes
const before = diffable
? await readTextForDiff(target.targetKey, this.config.diffBasisMaxBytes, signal)
: null
await writeFileAtomic(
target.targetKey,
content,