mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(history): raise the transcript head-read window to fit restart bookkeeping
Blank firstPrompt rows weren't all oversized messages -- traced one directly: a session restarted many times (mux deaths, redeploys) accumulates a batch of small bookkeeping lines (mode/permission-mode/ last-prompt/queue-operation, one batch per restart) ahead of the real first message. With enough restarts these alone crossed the old 16KB head-read window, so extraction found nothing even though the actual first message was tiny (measured case: ~17.5KB of bookkeeping pushed a 189-byte real message just past the boundary). Raise the head buffer from 16KB to 128KB (matching the existing precedent at the codex-history head-read a few hundred lines up) and fix three now-stale `> 16384`/`> 65536` fallback thresholds to reference headBuf.length instead of hardcoded numbers, so the tail-read fallbacks stay correctly scoped to "beyond what head already covered." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a2aed38073
commit
18b473f0e4
@@ -2760,7 +2760,7 @@ export function registerSessionRoutes(
|
|||||||
|
|
||||||
let foundContent = head ? hasConversation(head) : false;
|
let foundContent = head ? hasConversation(head) : false;
|
||||||
let tail: string | null = null;
|
let tail: string | null = null;
|
||||||
if (!foundContent && fileStat.size > 16384) {
|
if (!foundContent && fileStat.size > headBuf.length) {
|
||||||
const tailBuf = Buffer.alloc(32768);
|
const tailBuf = Buffer.alloc(32768);
|
||||||
tail = await readFileTail(filePath, tailBuf, fileStat.size);
|
tail = await readFileTail(filePath, tailBuf, fileStat.size);
|
||||||
if (tail) foundContent = hasConversation(tail);
|
if (tail) foundContent = hasConversation(tail);
|
||||||
@@ -2768,7 +2768,7 @@ export function registerSessionRoutes(
|
|||||||
if (!foundContent) continue;
|
if (!foundContent) continue;
|
||||||
|
|
||||||
if (head) firstPrompt = extractFirstUserPrompt(head);
|
if (head) firstPrompt = extractFirstUserPrompt(head);
|
||||||
if (!firstPrompt && fileStat.size > 65536) {
|
if (!firstPrompt && fileStat.size > headBuf.length) {
|
||||||
if (!tail) {
|
if (!tail) {
|
||||||
const tailBuf = Buffer.alloc(32768);
|
const tailBuf = Buffer.alloc(32768);
|
||||||
tail = await readFileTail(filePath, tailBuf, fileStat.size);
|
tail = await readFileTail(filePath, tailBuf, fileStat.size);
|
||||||
@@ -2778,9 +2778,9 @@ export function registerSessionRoutes(
|
|||||||
|
|
||||||
// COD-145: last (most recent) user prompt lives near the END of the file, so
|
// COD-145: last (most recent) user prompt lives near the END of the file, so
|
||||||
// prefer the tail. For large files where no tail was read yet, read one
|
// prefer the tail. For large files where no tail was read yet, read one
|
||||||
// (mirrors the firstPrompt > 65536 block). Small files fit in `head`, which
|
// (mirrors the firstPrompt > headBuf.length block). Small files fit in `head`,
|
||||||
// then contains the whole transcript — scan it for the last match instead.
|
// which then contains the whole transcript — scan it for the last match instead.
|
||||||
if (!tail && fileStat.size > 65536) {
|
if (!tail && fileStat.size > headBuf.length) {
|
||||||
const tailBuf = Buffer.alloc(32768);
|
const tailBuf = Buffer.alloc(32768);
|
||||||
tail = await readFileTail(filePath, tailBuf, fileStat.size);
|
tail = await readFileTail(filePath, tailBuf, fileStat.size);
|
||||||
}
|
}
|
||||||
@@ -2819,7 +2819,14 @@ export function registerSessionRoutes(
|
|||||||
app.get('/api/history/sessions', async (req) => {
|
app.get('/api/history/sessions', async (req) => {
|
||||||
const query = req.query as { projectKey?: string; offset?: string; limit?: string };
|
const query = req.query as { projectKey?: string; offset?: string; limit?: string };
|
||||||
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
||||||
const headBuf = Buffer.alloc(16384);
|
// 128KB (was 16KB): a session restarted many times over the course of a long
|
||||||
|
// conversation accumulates small bookkeeping lines (mode/permission-mode/
|
||||||
|
// last-prompt/queue-operation, one batch per restart) ahead of the real first
|
||||||
|
// message. 16KB was enough margin for a handful of restarts but not dozens —
|
||||||
|
// a genuinely tiny first message still came up blank because the metadata
|
||||||
|
// alone crossed the window. 128KB matches the existing precedent elsewhere in
|
||||||
|
// this file (line ~1431) rather than inventing a new size.
|
||||||
|
const headBuf = Buffer.alloc(131072);
|
||||||
// Multi-user: this scans the host-wide ~/.claude/projects tree, so a non-admin
|
// Multi-user: this scans the host-wide ~/.claude/projects tree, so a non-admin
|
||||||
// must only see history whose decoded workingDir is inside their own case space.
|
// must only see history whose decoded workingDir is inside their own case space.
|
||||||
// Do NOT trust the caller-supplied projectKey — confine on the decoded path.
|
// Do NOT trust the caller-supplied projectKey — confine on the decoded path.
|
||||||
@@ -2926,7 +2933,8 @@ export function registerSessionRoutes(
|
|||||||
const history: HistoryInput[] = [];
|
const history: HistoryInput[] = [];
|
||||||
try {
|
try {
|
||||||
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
||||||
const headBuf = Buffer.alloc(16384);
|
// 128KB (was 16KB) — see the sibling allocation above for why.
|
||||||
|
const headBuf = Buffer.alloc(131072);
|
||||||
const projectDirs = await fs.readdir(projectsDir);
|
const projectDirs = await fs.readdir(projectsDir);
|
||||||
for (const projDir of projectDirs) {
|
for (const projDir of projectDirs) {
|
||||||
const projPath = join(projectsDir, projDir);
|
const projPath = join(projectsDir, projDir);
|
||||||
|
|||||||
@@ -1395,6 +1395,40 @@ describe('session-routes', () => {
|
|||||||
expect(ids).toContain(noEntrypointId);
|
expect(ids).toContain(noEntrypointId);
|
||||||
expect(ids).not.toContain(sdkId);
|
expect(ids).not.toContain(sdkId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('finds the real first prompt past a large run of pre-message bookkeeping lines', async () => {
|
||||||
|
// A session restarted many times over a long conversation accumulates a batch
|
||||||
|
// of small bookkeeping lines (mode/permission-mode/last-prompt/queue-operation)
|
||||||
|
// per restart, ahead of the real first message. With enough restarts these can
|
||||||
|
// push the genuine first prompt past a 16KB head-read window even though the
|
||||||
|
// message itself is tiny — the row showed up blank despite having real content.
|
||||||
|
const home = process.env.HOME as string;
|
||||||
|
const projPath = join(home, '.claude', 'projects', 'proj-bookkeeping-test');
|
||||||
|
await mkdir(projPath, { recursive: true });
|
||||||
|
|
||||||
|
const sessionId = '66666666-6666-6666-6666-666666666666';
|
||||||
|
const bookkeepingLine = JSON.stringify({ type: 'mode', mode: 'normal', sessionId }) + '\n';
|
||||||
|
// > 16KB (the old head-read size) but well under 128KB (the new one).
|
||||||
|
const prefix = bookkeepingLine.repeat(Math.ceil(20000 / bookkeepingLine.length));
|
||||||
|
const realLine =
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'user',
|
||||||
|
entrypoint: 'cli',
|
||||||
|
message: { role: 'user', content: 'the real first message' },
|
||||||
|
}) + '\n';
|
||||||
|
expect(prefix.length).toBeGreaterThan(16384);
|
||||||
|
|
||||||
|
await writeFile(join(projPath, `${sessionId}.jsonl`), prefix + realLine);
|
||||||
|
|
||||||
|
const res = await harness.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/history/sessions?projectKey=proj-bookkeeping-test',
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const row = JSON.parse(res.body).data.sessions.find((s: { sessionId: string }) => s.sessionId === sessionId);
|
||||||
|
expect(row).toBeDefined();
|
||||||
|
expect(row.firstPrompt).toBe('the real first message');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== POST /api/sessions (with resumeSessionId) ==========
|
// ========== POST /api/sessions (with resumeSessionId) ==========
|
||||||
|
|||||||
Reference in New Issue
Block a user