mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 17:27:54 +00:00
Session-Level Conversation History Management (#1113)
This commit is contained in:
@@ -32,7 +32,6 @@ describe('Retry Utility Fallback Integration', () => {
|
||||
isDirectory: () => true,
|
||||
} as fs.Stats);
|
||||
config = new Config({
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
/**
|
||||
* Checks if a directory is within a git repository
|
||||
@@ -71,3 +72,19 @@ export function findGitRoot(directory: string): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current git branch, if in a git repository.
|
||||
*/
|
||||
export const getGitBranch = (cwd: string): string | undefined => {
|
||||
try {
|
||||
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
return branch || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
193
packages/core/src/utils/jsonl-utils.ts
Normal file
193
packages/core/src/utils/jsonl-utils.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Efficient JSONL (JSON Lines) file utilities.
|
||||
*
|
||||
* Reading operations:
|
||||
* - readLines() - Reads the first N lines efficiently using buffered I/O
|
||||
* - read() - Reads entire file into memory as array
|
||||
*
|
||||
* Writing operations:
|
||||
* - writeLine() - Async append with mutex-based concurrency control
|
||||
* - writeLineSync() - Sync append (use in non-async contexts)
|
||||
* - write() - Overwrites entire file with array of objects
|
||||
*
|
||||
* Utility operations:
|
||||
* - countLines() - Counts non-empty lines
|
||||
* - exists() - Checks if file exists and is non-empty
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
/**
|
||||
* A map of file paths to mutexes for preventing concurrent writes.
|
||||
*/
|
||||
const fileLocks = new Map<string, Mutex>();
|
||||
|
||||
/**
|
||||
* Gets or creates a mutex for a specific file path.
|
||||
*/
|
||||
function getFileLock(filePath: string): Mutex {
|
||||
if (!fileLocks.has(filePath)) {
|
||||
fileLocks.set(filePath, new Mutex());
|
||||
}
|
||||
return fileLocks.get(filePath)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the first N lines from a JSONL file efficiently.
|
||||
* Returns an array of parsed objects.
|
||||
*/
|
||||
export async function readLines<T = unknown>(
|
||||
filePath: string,
|
||||
count: number,
|
||||
): Promise<T[]> {
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
const results: T[] = [];
|
||||
for await (const line of rl) {
|
||||
if (results.length >= count) break;
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.length > 0) {
|
||||
results.push(JSON.parse(trimmed) as T);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error(
|
||||
`Error reading first ${count} lines from ${filePath}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all lines from a JSONL file.
|
||||
* Returns an array of parsed objects.
|
||||
*/
|
||||
export async function read<T = unknown>(filePath: string): Promise<T[]> {
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
const results: T[] = [];
|
||||
for await (const line of rl) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.length > 0) {
|
||||
results.push(JSON.parse(trimmed) as T);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error(`Error reading ${filePath}:`, error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a line to a JSONL file with concurrency control.
|
||||
* This method uses a mutex to ensure only one write happens at a time per file.
|
||||
*/
|
||||
export async function writeLine(
|
||||
filePath: string,
|
||||
data: unknown,
|
||||
): Promise<void> {
|
||||
const lock = getFileLock(filePath);
|
||||
await lock.runExclusive(() => {
|
||||
const line = `${JSON.stringify(data)}\n`;
|
||||
// Ensure directory exists before writing
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.appendFileSync(filePath, line, 'utf8');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version of writeLine for use in non-async contexts.
|
||||
* Uses a simple flag-based locking mechanism (less robust than async version).
|
||||
*/
|
||||
export function writeLineSync(filePath: string, data: unknown): void {
|
||||
const line = `${JSON.stringify(data)}\n`;
|
||||
// Ensure directory exists before writing
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.appendFileSync(filePath, line, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites a JSONL file with an array of objects.
|
||||
* Each object will be written as a separate line.
|
||||
*/
|
||||
export function write(filePath: string, data: unknown[]): void {
|
||||
const lines = data.map((item) => JSON.stringify(item)).join('\n');
|
||||
// Ensure directory exists before writing
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, `${lines}\n`, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of non-empty lines in a JSONL file.
|
||||
*/
|
||||
export async function countLines(filePath: string): Promise<number> {
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
for await (const line of rl) {
|
||||
if (line.trim().length > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error(`Error counting lines in ${filePath}:`, error);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a JSONL file exists and is not empty.
|
||||
*/
|
||||
export function exists(filePath: string): boolean {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
return stats.isFile() && stats.size > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ vi.mock('node:fs', () => {
|
||||
});
|
||||
}),
|
||||
existsSync: vi.fn((path: string) => mockFileSystem.has(path)),
|
||||
appendFileSync: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -38,7 +38,7 @@ export function tildeifyPath(path: string): string {
|
||||
* Shortens a path string if it exceeds maxLen, prioritizing the start and end segments.
|
||||
* Example: /path/to/a/very/long/file.txt -> /path/.../long/file.txt
|
||||
*/
|
||||
export function shortenPath(filePath: string, maxLen: number = 35): string {
|
||||
export function shortenPath(filePath: string, maxLen: number = 80): string {
|
||||
if (filePath.length <= maxLen) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
export const sessionId = randomUUID();
|
||||
Reference in New Issue
Block a user