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

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