























@@ -198,11 +198,11 @@ function createStartedThreadHarness(
198198async notify(notification: CodexServerNotification) {
199199await notify(notification);
200200},
201-async completeTurn(status: "completed" | "failed" = "completed") {
201+async completeTurn(status: "completed" | "failed" = "completed", threadId = "thread-1") {
202202await notify({
203203method: "turn/completed",
204204params: {
205-threadId: "thread-1",
205+ threadId,
206206turnId: "turn-1",
207207turn: {
208208id: "turn-1",
@@ -545,6 +545,201 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
545545await secondRun;
546546});
547547548+it.each([
549+[
550+"token",
551+`${JSON.stringify({
552+ payload: {
553+ type: "token_count",
554+ info: {
555+ last_token_usage: {
556+ total_tokens: 300_000,
557+ },
558+ },
559+ },
560+ })}\n`,
561+"1mb",
562+],
563+["byte", "x".repeat(2_000), 1_000],
564+] as const)(
565+"resumes a matching thread-bootstrap binding even when the bootstrap turn exceeded the native %s guard",
566+async (_guard, rolloutContent, maxActiveTranscriptBytes) => {
567+const sessionFile = path.join(tempDir, "session.jsonl");
568+const workspaceDir = path.join(tempDir, "workspace");
569+const agentDir = path.join(tempDir, "agent");
570+await writeCodexAppServerBinding(sessionFile, {
571+threadId: "thread-bootstrapped",
572+cwd: workspaceDir,
573+dynamicToolsFingerprint: "[]",
574+contextEngine: {
575+schemaVersion: 1,
576+engineId: "lossless-claw",
577+policyFingerprint:
578+'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
579+projection: {
580+schemaVersion: 1,
581+mode: "thread_bootstrap",
582+epoch: "epoch-1",
583+},
584+},
585+});
586+await fs.writeFile(
587+path.join(path.dirname(sessionFile), "sessions.json"),
588+JSON.stringify({
589+"agent:main:session-1": {
590+ sessionFile,
591+totalTokens: 12_000,
592+},
593+}),
594+);
595+const rolloutDir = path.join(agentDir, "codex-home", "sessions");
596+await fs.mkdir(rolloutDir, { recursive: true });
597+await fs.writeFile(
598+path.join(rolloutDir, "rollout-thread-bootstrapped.jsonl"),
599+rolloutContent,
600+);
601+const contextEngine = createContextEngine({
602+assemble: vi.fn(async ({ prompt }) => ({
603+messages: [
604+assistantMessage("already bootstrapped context", 10),
605+userMessage(prompt ?? "", 11),
606+],
607+estimatedTokens: 42,
608+systemPromptAddition: "context-engine system",
609+contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-1" },
610+})),
611+});
612+const harness = createStartedThreadHarness(async (method) => {
613+if (method === "thread/resume") {
614+return threadStartResult("thread-bootstrapped");
615+}
616+if (method === "thread/start") {
617+return threadStartResult("thread-fresh");
618+}
619+return undefined;
620+});
621+const params = createParams(sessionFile, workspaceDir);
622+params.agentDir = agentDir;
623+params.contextEngine = contextEngine;
624+params.config = {
625+agents: {
626+defaults: {
627+compaction: {
628+truncateAfterCompaction: true,
629+ maxActiveTranscriptBytes,
630+},
631+},
632+},
633+} as EmbeddedRunAttemptParams["config"];
634+635+const run = runCodexAppServerAttempt(params);
636+await harness.waitForMethod("turn/start");
637+638+expect(harness.requests.map((request) => request.method)).toEqual([
639+"thread/resume",
640+"turn/start",
641+]);
642+const inputText = getRequestInputText(harness);
643+expect(inputText).not.toContain("OpenClaw assembled context for this turn:");
644+expect(inputText).not.toContain("already bootstrapped context");
645+expect(inputText).toBe("hello");
646+647+await harness.completeTurn("completed", "thread-bootstrapped");
648+await run;
649+},
650+);
651+652+it("projects mirrored history when an oversized thread-bootstrap binding has no active context engine", async () => {
653+const sessionFile = path.join(tempDir, "session.jsonl");
654+const workspaceDir = path.join(tempDir, "workspace");
655+const agentDir = path.join(tempDir, "agent");
656+const sessionManager = SessionManager.open(sessionFile);
657+sessionManager.appendMessage(
658+userMessage("previous stale-bootstrap request", Date.now()) as never,
659+);
660+sessionManager.appendMessage(
661+assistantMessage("previous stale-bootstrap answer", Date.now() + 1) as never,
662+);
663+await writeCodexAppServerBinding(sessionFile, {
664+threadId: "thread-stale-bootstrap",
665+cwd: workspaceDir,
666+dynamicToolsFingerprint: "[]",
667+contextEngine: {
668+schemaVersion: 1,
669+engineId: "lossless-claw",
670+policyFingerprint:
671+'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
672+projection: {
673+schemaVersion: 1,
674+mode: "thread_bootstrap",
675+epoch: "epoch-stale",
676+},
677+},
678+});
679+await fs.writeFile(
680+path.join(path.dirname(sessionFile), "sessions.json"),
681+JSON.stringify({
682+"agent:main:session-1": {
683+ sessionFile,
684+totalTokens: 12_000,
685+},
686+}),
687+);
688+const rolloutDir = path.join(agentDir, "codex-home", "sessions");
689+await fs.mkdir(rolloutDir, { recursive: true });
690+await fs.writeFile(
691+path.join(rolloutDir, "rollout-thread-stale-bootstrap.jsonl"),
692+`${JSON.stringify({
693+ payload: {
694+ type: "token_count",
695+ info: {
696+ last_token_usage: {
697+ total_tokens: 300_000,
698+ },
699+ },
700+ },
701+ })}\n`,
702+);
703+const harness = createStartedThreadHarness(async (method) => {
704+if (method === "thread/resume") {
705+return threadStartResult("thread-stale-bootstrap");
706+}
707+if (method === "thread/start") {
708+return threadStartResult("thread-fresh");
709+}
710+return undefined;
711+});
712+const params = createParams(sessionFile, workspaceDir);
713+params.agentDir = agentDir;
714+params.config = {
715+agents: {
716+defaults: {
717+compaction: {
718+truncateAfterCompaction: true,
719+maxActiveTranscriptBytes: "1mb",
720+},
721+},
722+},
723+} as EmbeddedRunAttemptParams["config"];
724+725+const run = runCodexAppServerAttempt(params);
726+await harness.waitForMethod("turn/start");
727+728+expect(harness.requests.map((request) => request.method)).toEqual([
729+"thread/start",
730+"turn/start",
731+]);
732+const inputText = getRequestInputText(harness);
733+expect(inputText).toContain("OpenClaw assembled context for this turn:");
734+expect(inputText).toContain("previous stale-bootstrap request");
735+expect(inputText).toContain("previous stale-bootstrap answer");
736+expect(inputText).toContain("Current user request:");
737+expect(inputText).toContain("hello");
738+739+await harness.completeTurn("completed", "thread-fresh");
740+await run;
741+});
742+548743it("starts a fresh Codex thread and reprojects when context-engine epoch changes", async () => {
549744const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined);
550745const sessionFile = path.join(tempDir, "session.jsonl");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。