Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
748c888
fix(serve): allow bounded reads of large text files
doudouOUC Jul 28, 2026
e784e6d
fix(serve): bound large-text reads by scan cost, not by which knob wa…
doudouOUC Jul 28, 2026
f55c867
refactor(core): thread the descriptor instead of forking text-read he…
doudouOUC Jul 28, 2026
a74eeea
refactor(core): make CoreReadTextFileHandleRequest standalone
doudouOUC Jul 29, 2026
a1b2188
Merge remote-tracking branch 'origin/main' into pr-a-descriptor-threa…
Jul 29, 2026
63822f6
fix(serve): address review feedback on decoded-output cap, lineEnding…
Jul 29, 2026
63d49c4
fix(serve): scope PR to descriptor threading, revert #7947 behavior r…
Jul 29, 2026
bc4d4c6
test(core): port deep-read and encoding-probe tests to the handle pat…
Jul 29, 2026
9c0e08d
test(core): pin handle read-to-EOF and unlimited-limit contracts (#7967)
qwen-code-ci-bot Jul 29, 2026
224db1f
Merge branch 'main' into pr-a-descriptor-threading
wenshao Jul 29, 2026
67ad71f
docs(serve): disclose handle-read behaviour deltas and add boundary t…
Jul 30, 2026
cdbdfde
test(core): add positive handle-read test for Infinity and omitted li…
Jul 30, 2026
1e22035
fix(core): satisfy ReadTextRangeResult type in handle-read test mock …
Jul 30, 2026
dc0604c
fix(core): bound handle reads to stat size and reject Infinity limit …
Jul 30, 2026
88e743d
Merge remote-tracking branch 'origin/main' into pr-a-descriptor-threa…
Jul 30, 2026
20d606b
fix(core): satisfy ReadTextRangeResult type in fileSystemService test…
Jul 30, 2026
d2b36fc
Merge branch 'main' into pr-a-descriptor-threading
qwen-code-dev-bot Jul 30, 2026
b3dafee
refactor(core): reuse detectLineEndingFromContent in whole-file fast …
qwen-code-dev-bot Jul 30, 2026
2558093
fix(core): export ReadTextRangeResult type and cover CRLF detection (…
qwen-code-dev-bot Jul 31, 2026
af5da8e
test(core): cover whole-file lineEnding and truncatedByBytes shape (#…
Jul 31, 2026
6532ddf
Merge branch 'main' into pr-a-descriptor-threading
qwen-code-dev-bot Jul 31, 2026
3c46060
Merge branch 'main' into pr-a-descriptor-threading
qwen-code-dev-bot Jul 31, 2026
7f5e851
Merge branch 'main' into pr-a-descriptor-threading
qwen-code-dev-bot Jul 31, 2026
9e01ff2
Merge branch 'main' into pr-a-descriptor-threading
qwen-code-dev-bot Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/cli/src/serve/routes/workspace-file-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,23 @@ describe('GET /file', () => {
expect(res.body.errorKind).toBe('binary_file');
});

it('returns 422 binary_file for large non-UTF-8 text even with a finite limit', async () => {
const { MAX_READ_BYTES } = await import('../fs/policy.js');
const body = Buffer.concat([
Buffer.from([0xff, 0xfe]),
Buffer.from('中文日志行\n'.repeat(30_000), 'utf16le'),
]);
expect(body.length).toBeGreaterThan(MAX_READ_BYTES);
await fsp.writeFile(path.join(h.workspace, 'large-utf16.txt'), body);

const res = await request(h.app)
.get('/file?path=large-utf16.txt&line=1&limit=20')
.set('Host', loopbackHost());

expect(res.status).toBe(422);
expect(res.body.errorKind).toBe('binary_file');
});

it('truncates content above maxBytes and reports truncated=true', async () => {
await fsp.writeFile(path.join(h.workspace, 'big.txt'), 'a'.repeat(2048));
const res = await request(h.app)
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export {
LargeNonUtf8TextError,
TextScanBudgetExceededError,
} from './utils/read-text-range.js';
export type { ReadTextRangeResult } from './utils/read-text-range.js';
export { isUtf8CompatibleEncoding } from './utils/encoding.js';
export * from './services/gitWorktreeService.js';
export { DEFAULT_MAX_TOOL_CALLS_PER_TURN } from './services/loopDetectionService.js';
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/services/fileSystemService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 1,
originalLineCountExact: true,
truncatedByBytes: false,
});

const result = await fileSystem.readTextFile({ path: '/test/file.txt' });
Expand All @@ -99,6 +101,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 100,
originalLineCountExact: true,
truncatedByBytes: false,
});

const result = await fileSystem.readTextFile({
Expand All @@ -121,6 +125,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 100,
originalLineCountExact: true,
truncatedByBytes: false,
});

await fileSystem.readTextFile({
Expand All @@ -141,6 +147,7 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 100,
originalLineCountExact: true,
truncatedByBytes: true,
});

Expand All @@ -167,6 +174,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 1,
originalLineCountExact: true,
truncatedByBytes: false,
});

await fileSystem.readTextFile({
Expand Down Expand Up @@ -312,6 +321,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'gb18030',
originalLineCount: 1,
originalLineCountExact: true,
truncatedByBytes: false,
});

const result = await fileSystem.readTextFile({ path: '/test/gbk.txt' });
Expand Down Expand Up @@ -793,6 +804,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 3,
originalLineCountExact: true,
truncatedByBytes: false,
});

const result = await fileSystem.readTextFile({ path: '/test/file.txt' });
Expand All @@ -806,6 +819,8 @@ describe('StandardFileSystemService', () => {
bom: false,
encoding: 'utf-8',
originalLineCount: 3,
originalLineCountExact: true,
truncatedByBytes: false,
});

const result = await fileSystem.readTextFile({ path: '/test/file.txt' });
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/utils/fileUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,27 @@ describe('fileUtils', () => {
).rejects.toThrow(/abort/i);
});

it('reports lineEnding and truncatedByBytes on an unbounded read', async () => {
actualNodeFs.writeFileSync(
testTextFilePath,
'alpha\r\nbravo\r\ncharlie\r\n',
);

const whole = await readFileWithLineAndLimit({
path: testTextFilePath,
limit: Number.POSITIVE_INFINITY,
});
const range = await readFileWithLineAndLimit({
path: testTextFilePath,
limit: 3,
});

expect(whole.lineEnding).toBe('crlf');
expect(whole.truncatedByBytes).toBe(false);
expect(whole.lineEnding).toBe(range.lineEnding);
expect(whole.truncatedByBytes).toBe(range.truncatedByBytes);
});

it('should use provided stats when reading with line and byte limits', async () => {
actualNodeFs.writeFileSync(testTextFilePath, 'hello\nworld');
const stats = actualNodeFs.statSync(testTextFilePath);
Expand Down
21 changes: 10 additions & 11 deletions packages/core/src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ import {
import { VISION_BRIDGE_MAX_IMAGES } from '../services/visionBridge/vision-bridge-constants.js';
import type { VisionBridgePdfContinuation } from '../services/visionBridge/vision-bridge-service.js';
import { readNotebookWithMetadata } from './notebook.js';
import { readTextRange } from './read-text-range.js';
import {
readTextRange,
type ReadTextRangeResult,
detectLineEndingFromContent,
} from './read-text-range.js';
import {
DEFAULT_RANGE_READ_BYTES,
TEXT_RANGE_FAST_PATH_MAX_SIZE,
Expand Down Expand Up @@ -330,15 +334,7 @@ export async function readFileWithLineAndLimit(params: {
maxOutputBytes?: number;
signal?: AbortSignal;
stats?: import('node:fs').Stats;
}): Promise<{
content: string;
bom?: boolean;
encoding?: string;
originalLineCount: number;
originalLineCountExact?: boolean;
lineEnding?: 'crlf' | 'lf';
truncatedByBytes?: boolean;
}> {
}): Promise<ReadTextRangeResult> {
const { path: filePath, limit, line, maxOutputBytes, signal } = params;
const stats = params.stats ?? (await fs.promises.stat(filePath));
if (
Expand Down Expand Up @@ -372,12 +368,15 @@ export async function readFileWithLineAndLimit(params: {
const actualStartLine = Math.min(startLine, originalLineCount);
const selectedLines = lines.slice(actualStartLine, endLine);

const joined = selectedLines.join('\n');
return {
content: selectedLines.join('\n'),
content: joined,
bom,
encoding,
originalLineCount,
originalLineCountExact: true,
truncatedByBytes: false,
lineEnding: detectLineEndingFromContent(joined),
};
}

Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/utils/read-text-range.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { iconvEncode } from './iconvHelper.js';
import {
CursorNotAtLineBoundaryError,
LargeNonUtf8TextError,
detectLineEndingFromContent,
readTextCursorWindowFromHandle,
readTextRange,
readTextRangeFromHandle,
Expand Down Expand Up @@ -995,3 +996,13 @@ describe('readTextCursorWindowFromHandle', () => {
});
});
});

describe('detectLineEndingFromContent', () => {
it('reports crlf for small CRLF content', () => {
expect(detectLineEndingFromContent('line1\r\nline2\r\n')).toBe('crlf');
});

it('reports lf when no CRLF is present', () => {
expect(detectLineEndingFromContent('line1\nline2\n')).toBe('lf');
});
});
2 changes: 1 addition & 1 deletion packages/core/src/utils/read-text-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,6 @@ function truncateUtf8(
};
}

function detectLineEndingFromContent(content: string): 'crlf' | 'lf' {
export function detectLineEndingFromContent(content: string): 'crlf' | 'lf' {
return content.includes('\r\n') ? 'crlf' : 'lf';
}
Loading