mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
fix(memory-ingest): parse the current Codex response_item rollout shape
Fixes #2105. Codex rollout JSONL moved to { type: 'response_item', payload: { type: 'message', role, content: [...] } }; the parser's legacy payload.message branch never fired on it, so every Codex session imported as an empty shell (message_count: 0 — 243/243 sessions on the reporting machine). Both shapes now parse; non-message response_items (reasoning etc.) are ignored. parseTranscriptJsonl exported for direct unit tests (CLI path unchanged — import.meta.main guard). Note: #2104's staging-in-gitignored-tree half is already defended on main (--include-gitignored + GIT_CEILING_DIRECTORIES, #2144, plus the #2486 reconcile guard) — verified, no change needed; it moves to the close-only roster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c2cdf65176
commit
00d0115ac7
@@ -543,7 +543,7 @@ interface ParsedSession {
|
||||
partial: boolean;
|
||||
}
|
||||
|
||||
function parseTranscriptJsonl(path: string): ParsedSession | null {
|
||||
export function parseTranscriptJsonl(path: string): ParsedSession | null {
|
||||
// Best-effort tolerant parser. Handles truncated last lines (D10 partial-flag).
|
||||
let raw: string;
|
||||
try {
|
||||
@@ -619,7 +619,7 @@ function parseTranscriptJsonl(path: string): ParsedSession | null {
|
||||
const tool = rec?.name || rec?.tool || rec?.tool_call?.name || "tool";
|
||||
bodyParts.push(`### Tool call: ${tool}`);
|
||||
} else if (isCodex && rec?.payload?.message) {
|
||||
// Codex shape: each record has payload.message
|
||||
// Legacy Codex shape: each record has payload.message
|
||||
const msg = rec.payload.message;
|
||||
const role = msg.role || "user";
|
||||
const content = extractContentText(msg);
|
||||
@@ -627,6 +627,18 @@ function parseTranscriptJsonl(path: string): ParsedSession | null {
|
||||
bodyParts.push(`## ${role.charAt(0).toUpperCase() + role.slice(1)}\n\n${content}`);
|
||||
messageCount++;
|
||||
}
|
||||
} else if (isCodex && rec?.type === "response_item" && rec?.payload?.type === "message") {
|
||||
// Current Codex rollout shape (#2105): records are
|
||||
// { type: 'response_item', payload: { type: 'message', role, content: [...] } }.
|
||||
// The legacy payload.message branch never fires on these, which rendered
|
||||
// every Codex session as an empty shell (message_count: 0, 243/243 on
|
||||
// the reporting machine). Flatten payload.content like the Claude branch.
|
||||
const role = rec.payload.role || "user";
|
||||
const content = extractContentText(rec.payload);
|
||||
if (content) {
|
||||
bodyParts.push(`## ${role.charAt(0).toUpperCase() + role.slice(1)}\n\n${content}`);
|
||||
messageCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -818,3 +818,46 @@ exit 0
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
// #2105: current Codex rollout records are
|
||||
// { type: 'response_item', payload: { type: 'message', role, content: [...] } }
|
||||
// — the legacy payload.message branch never fired on them, so every Codex
|
||||
// session imported as an empty shell (message_count: 0, 243/243 on the
|
||||
// reporting machine).
|
||||
describe("#2105 codex response_item rollout shape", () => {
|
||||
it("extracts messages from response_item records", async () => {
|
||||
const { parseTranscriptJsonl } = await import("../bin/gstack-memory-ingest");
|
||||
const dir = mkdtempSync(join(tmpdir(), "ingest-2105-"));
|
||||
const file = join(dir, "rollout-2026-06-01.jsonl");
|
||||
writeFileSync(file, [
|
||||
JSON.stringify({ type: "session_meta", payload: { id: "s1", cwd: "/tmp/x" }, timestamp: "2026-06-01T00:00:00Z" }),
|
||||
JSON.stringify({ type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: "hello codex" }] } }),
|
||||
JSON.stringify({ type: "response_item", payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "hello human" }] } }),
|
||||
// Non-message response_items must not count as messages.
|
||||
JSON.stringify({ type: "response_item", payload: { type: "reasoning", summary: [] } }),
|
||||
].join("\n") + "\n");
|
||||
|
||||
const parsed = parseTranscriptJsonl(file)!;
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed.agent).toBe("codex");
|
||||
expect(parsed.message_count).toBe(2);
|
||||
expect(parsed.body).toContain("## User\n\nhello codex");
|
||||
expect(parsed.body).toContain("## Assistant\n\nhello human");
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("legacy payload.message shape still parses", async () => {
|
||||
const { parseTranscriptJsonl } = await import("../bin/gstack-memory-ingest");
|
||||
const dir = mkdtempSync(join(tmpdir(), "ingest-2105-legacy-"));
|
||||
const file = join(dir, "rollout-legacy.jsonl");
|
||||
writeFileSync(file, [
|
||||
JSON.stringify({ type: "session_meta", payload: { id: "s2", cwd: "/tmp/y" }, timestamp: "2026-06-01T00:00:00Z" }),
|
||||
JSON.stringify({ payload: { message: { role: "user", content: "old shape" } } }),
|
||||
].join("\n") + "\n");
|
||||
|
||||
const parsed = parseTranscriptJsonl(file)!;
|
||||
expect(parsed.message_count).toBe(1);
|
||||
expect(parsed.body).toContain("## User\n\nold shape");
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user