


























@@ -313,4 +313,178 @@ describe("Codex trajectory recorder", () => {
313313expect(parsed.data.droppedFields).toContain("messagesSnapshot");
314314expect(Buffer.byteLength(JSON.stringify(parsed), "utf8")).toBeLessThanOrEqual(256 * 1024);
315315});
316+317+it("drops oversized preserved fields when needed to keep completion events bounded", async () => {
318+const tmpDir = makeTempDir();
319+const sessionFile = path.join(tmpDir, "session.jsonl");
320+const attempt = {
321+ sessionFile,
322+sessionId: "session-1",
323+sessionKey: "agent:main:session-1",
324+runId: "run-1",
325+provider: "codex",
326+modelId: "gpt-5.4",
327+model: { api: "responses" },
328+} as never;
329+const oversizedUsage = Object.fromEntries(
330+Array.from({ length: 100 }, (_value, index) => [`field-${index}`, "x".repeat(5_000)]),
331+);
332+const recorder = createCodexTrajectoryRecorder({
333+cwd: tmpDir,
334+ attempt,
335+env: {},
336+});
337+338+const trajectoryRecorder = expectTrajectoryRecorder(recorder);
339+recordCodexTrajectoryCompletion(trajectoryRecorder, {
340+ attempt,
341+threadId: "thread-1",
342+turnId: "turn-1",
343+timedOut: false,
344+result: {
345+aborted: false,
346+attemptUsage: oversizedUsage,
347+assistantTexts: ["x".repeat(32_000)],
348+messagesSnapshot: [{ role: "assistant", content: "x".repeat(32_000) }],
349+} as never,
350+});
351+await trajectoryRecorder.flush();
352+353+const parsed = JSON.parse(
354+fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
355+);
356+expect(parsed.data).toMatchObject({
357+truncated: true,
358+reason: "trajectory-event-size-limit",
359+});
360+expect(parsed.data.usage).toBeUndefined();
361+expect(parsed.data.droppedFields).toEqual(
362+expect.arrayContaining(["usage", "assistantTexts", "messagesSnapshot"]),
363+);
364+expect(Buffer.byteLength(JSON.stringify(parsed), "utf8")).toBeLessThanOrEqual(256 * 1024);
365+});
366+367+it("preserves usage on non-final oversized model completion events", async () => {
368+const tmpDir = makeTempDir();
369+const sessionFile = path.join(tmpDir, "session.jsonl");
370+const attempt = {
371+ sessionFile,
372+sessionId: "session-1",
373+sessionKey: "agent:main:session-1",
374+runId: "run-1",
375+provider: "codex",
376+modelId: "gpt-5.4",
377+model: { api: "responses" },
378+} as never;
379+const firstUsage = {
380+input: 384_954,
381+output: 5_624,
382+cacheRead: 333_824,
383+reasoningTokens: 2_038,
384+total: 724_402,
385+};
386+const secondUsage = { input: 12, output: 3, total: 15 };
387+const recorder = createCodexTrajectoryRecorder({
388+cwd: tmpDir,
389+ attempt,
390+env: {},
391+});
392+393+const trajectoryRecorder = expectTrajectoryRecorder(recorder);
394+recordCodexTrajectoryCompletion(trajectoryRecorder, {
395+ attempt,
396+threadId: "thread-1",
397+turnId: "turn-1",
398+timedOut: false,
399+result: {
400+aborted: false,
401+attemptUsage: firstUsage,
402+assistantTexts: ["first"],
403+messagesSnapshot: Array.from({ length: 20 }, (_value, index) => ({
404+role: index % 2 === 0 ? "user" : "assistant",
405+content: `message-${index} ${"x".repeat(32_000)}`,
406+})),
407+} as never,
408+});
409+recordCodexTrajectoryCompletion(trajectoryRecorder, {
410+ attempt,
411+threadId: "thread-1",
412+turnId: "turn-2",
413+timedOut: false,
414+result: {
415+aborted: false,
416+attemptUsage: secondUsage,
417+assistantTexts: ["final answer"],
418+messagesSnapshot: [{ role: "assistant", content: "final answer" }],
419+} as never,
420+});
421+await trajectoryRecorder.flush();
422+423+const events = fs
424+.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8")
425+.trim()
426+.split(/\r?\n/u)
427+.map((line) => JSON.parse(line));
428+expect(events).toHaveLength(2);
429+expect(events[0].data).toMatchObject({
430+truncated: true,
431+usage: firstUsage,
432+});
433+expect(events[1].data).toMatchObject({
434+turnId: "turn-2",
435+usage: secondUsage,
436+assistantTexts: ["final answer"],
437+});
438+expect(events[1].data.truncated).toBeUndefined();
439+});
440+441+it("redacts secrets before preserving usage in truncated completion events", async () => {
442+const tmpDir = makeTempDir();
443+const sessionFile = path.join(tmpDir, "session.jsonl");
444+const attempt = {
445+ sessionFile,
446+sessionId: "session-1",
447+sessionKey: "agent:main:session-1",
448+runId: "run-1",
449+provider: "codex",
450+modelId: "gpt-5.4",
451+model: { api: "responses" },
452+} as never;
453+const recorder = createCodexTrajectoryRecorder({
454+cwd: tmpDir,
455+ attempt,
456+env: {},
457+});
458+459+const trajectoryRecorder = expectTrajectoryRecorder(recorder);
460+recordCodexTrajectoryCompletion(trajectoryRecorder, {
461+ attempt,
462+threadId: "thread-1",
463+turnId: "turn-1",
464+timedOut: false,
465+result: {
466+aborted: false,
467+attemptUsage: {
468+total: 1,
469+apiKey: "sk-test-secret-token",
470+authorization: "Bearer sk-other-secret-token",
471+},
472+assistantTexts: ["done"],
473+messagesSnapshot: Array.from({ length: 20 }, (_value, index) => ({
474+role: index % 2 === 0 ? "user" : "assistant",
475+content: `message-${index} ${"x".repeat(32_000)}`,
476+})),
477+} as never,
478+});
479+await trajectoryRecorder.flush();
480+481+const parsed = JSON.parse(
482+fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
483+);
484+const preservedUsage = JSON.stringify(parsed.data.usage);
485+expect(parsed.data.truncated).toBe(true);
486+expect(preservedUsage).toContain("redacted");
487+expect(preservedUsage).not.toContain("sk-test-secret-token");
488+expect(preservedUsage).not.toContain("sk-other-secret-token");
489+});
316490});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。