
























@@ -4470,6 +4470,180 @@ describe("runCodexAppServerAttempt", () => {
44704470expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
44714471});
447244724473+it("starts a fresh Codex thread for legacy context-engine sidecars without metadata", async () => {
4474+const sessionFile = path.join(tempDir, "session.jsonl");
4475+const workspaceDir = path.join(tempDir, "workspace");
4476+await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
4477+const params = createParams(sessionFile, workspaceDir);
4478+params.contextEngine = {
4479+info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
4480+assemble: vi.fn(),
4481+compact: vi.fn(),
4482+} as never;
4483+params.contextTokenBudget = 400_000;
4484+const appServer = createThreadLifecycleAppServerOptions();
4485+const request = vi.fn(async (method: string) => {
4486+if (method === "thread/start") {
4487+return threadStartResult("thread-fresh");
4488+}
4489+throw new Error(`unexpected method: ${method}`);
4490+});
4491+4492+const binding = await startOrResumeThread({
4493+client: { request } as never,
4494+ params,
4495+cwd: workspaceDir,
4496+dynamicTools: [],
4497+ appServer,
4498+});
4499+4500+expect(binding.threadId).toBe("thread-fresh");
4501+expect(binding.lifecycle).toEqual({
4502+action: "started",
4503+rotatedContextEngineBinding: true,
4504+});
4505+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]);
4506+const savedBinding = await readCodexAppServerBinding(sessionFile);
4507+expect(savedBinding?.contextEngine?.engineId).toBe("lossless-claw");
4508+expect(savedBinding?.contextEngine?.policyFingerprint).toContain('"contextTokenBudget":400000');
4509+});
4510+4511+it("resumes a Codex thread when context-engine sidecar metadata is compatible", async () => {
4512+const sessionFile = path.join(tempDir, "session.jsonl");
4513+const workspaceDir = path.join(tempDir, "workspace");
4514+const contextEngine = {
4515+schemaVersion: 1 as const,
4516+engineId: "lossless-claw",
4517+policyFingerprint:
4518+'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"contextTokenBudget":400000,"projectionMaxChars":1000000}',
4519+};
4520+await writeExistingBinding(sessionFile, workspaceDir, {
4521+dynamicToolsFingerprint: "[]",
4522+ contextEngine,
4523+});
4524+const params = createParams(sessionFile, workspaceDir);
4525+params.contextEngine = {
4526+info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
4527+assemble: vi.fn(),
4528+compact: vi.fn(),
4529+} as never;
4530+params.contextTokenBudget = 400_000;
4531+const appServer = createThreadLifecycleAppServerOptions();
4532+const request = vi.fn(async (method: string) => {
4533+if (method === "thread/resume") {
4534+return threadStartResult("thread-existing");
4535+}
4536+throw new Error(`unexpected method: ${method}`);
4537+});
4538+4539+const binding = await startOrResumeThread({
4540+client: { request } as never,
4541+ params,
4542+cwd: workspaceDir,
4543+dynamicTools: [],
4544+ appServer,
4545+});
4546+4547+expect(binding.threadId).toBe("thread-existing");
4548+expect(binding.lifecycle).toEqual({ action: "resumed" });
4549+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/resume"]);
4550+});
4551+4552+it("starts a fresh Codex thread when context-engine sidecar metadata is no longer active", async () => {
4553+const sessionFile = path.join(tempDir, "session.jsonl");
4554+const workspaceDir = path.join(tempDir, "workspace");
4555+await writeExistingBinding(sessionFile, workspaceDir, {
4556+dynamicToolsFingerprint: "[]",
4557+contextEngine: {
4558+schemaVersion: 1,
4559+engineId: "lossless-claw",
4560+policyFingerprint:
4561+'{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"contextTokenBudget":400000,"projectionMaxChars":1000000}',
4562+},
4563+});
4564+const params = createParams(sessionFile, workspaceDir);
4565+const appServer = createThreadLifecycleAppServerOptions();
4566+const request = vi.fn(async (method: string) => {
4567+if (method === "thread/start") {
4568+return threadStartResult("thread-fresh");
4569+}
4570+throw new Error(`unexpected method: ${method}`);
4571+});
4572+4573+const binding = await startOrResumeThread({
4574+client: { request } as never,
4575+ params,
4576+cwd: workspaceDir,
4577+dynamicTools: [],
4578+ appServer,
4579+});
4580+4581+expect(binding.threadId).toBe("thread-fresh");
4582+expect(binding.lifecycle).toEqual({
4583+action: "started",
4584+rotatedContextEngineBinding: true,
4585+});
4586+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]);
4587+const savedBinding = await readCodexAppServerBinding(sessionFile);
4588+expect(savedBinding?.contextEngine).toBeUndefined();
4589+});
4590+4591+it("starts a fresh Codex thread when context-engine policy metadata changes", async () => {
4592+const sessionFile = path.join(tempDir, "session.jsonl");
4593+const workspaceDir = path.join(tempDir, "workspace");
4594+await writeExistingBinding(sessionFile, workspaceDir, {
4595+dynamicToolsFingerprint: "[]",
4596+contextEngine: {
4597+schemaVersion: 1,
4598+engineId: "lossless-claw",
4599+policyFingerprint:
4600+'{"schemaVersion":1,"engineId":"lossless-claw","engineVersion":"1.0.0","ownsCompaction":true,"turnMaintenanceMode":"foreground","citationsMode":"inline","contextTokenBudget":400000,"projectionMaxChars":1000000}',
4601+},
4602+});
4603+const params = createParams(sessionFile, workspaceDir);
4604+params.contextEngine = {
4605+info: {
4606+id: "lossless-claw",
4607+name: "Lossless Claw",
4608+version: "1.0.1",
4609+ownsCompaction: true,
4610+turnMaintenanceMode: "foreground",
4611+},
4612+assemble: vi.fn(),
4613+compact: vi.fn(),
4614+} as never;
4615+params.config = { memory: { citations: "inline" } } as never;
4616+params.contextTokenBudget = 400_000;
4617+const appServer = createThreadLifecycleAppServerOptions();
4618+const request = vi.fn(async (method: string) => {
4619+if (method === "thread/start") {
4620+return threadStartResult("thread-fresh");
4621+}
4622+throw new Error(`unexpected method: ${method}`);
4623+});
4624+4625+const binding = await startOrResumeThread({
4626+client: { request } as never,
4627+ params,
4628+cwd: workspaceDir,
4629+dynamicTools: [],
4630+ appServer,
4631+});
4632+4633+expect(binding.threadId).toBe("thread-fresh");
4634+expect(binding.lifecycle).toEqual({
4635+action: "started",
4636+rotatedContextEngineBinding: true,
4637+});
4638+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]);
4639+const savedBinding = await readCodexAppServerBinding(sessionFile);
4640+expect(savedBinding?.contextEngine?.policyFingerprint).toContain('"engineVersion":"1.0.1"');
4641+expect(savedBinding?.contextEngine?.policyFingerprint).toContain(
4642+'"turnMaintenanceMode":"foreground"',
4643+);
4644+expect(savedBinding?.contextEngine?.policyFingerprint).toContain('"citationsMode":"inline"');
4645+});
4646+44734647it("keeps the previous dynamic tool fingerprint for transient no-tool maintenance turns", async () => {
44744648const sessionFile = path.join(tempDir, "session.jsonl");
44754649const workspaceDir = path.join(tempDir, "workspace");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。