























@@ -179,6 +179,44 @@ async function writeExistingBinding(
179179});
180180}
181181182+function createThreadLifecycleAppServerOptions(): Parameters<
183+typeof startOrResumeThread
184+>[0]["appServer"] {
185+return {
186+start: {
187+transport: "stdio",
188+command: "codex",
189+args: ["app-server"],
190+headers: {},
191+},
192+requestTimeoutMs: 60_000,
193+approvalPolicy: "never",
194+approvalsReviewer: "user",
195+sandbox: "workspace-write",
196+};
197+}
198+199+function createMessageDynamicTool(
200+description: string,
201+actions: string[] = ["send"],
202+): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
203+return {
204+name: "message",
205+ description,
206+inputSchema: {
207+type: "object",
208+properties: {
209+action: {
210+type: "string",
211+enum: actions,
212+},
213+},
214+required: ["action"],
215+additionalProperties: false,
216+},
217+};
218+}
219+182220describe("runCodexAppServerAttempt", () => {
183221beforeEach(async () => {
184222tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-run-"));
@@ -731,6 +769,76 @@ describe("runCodexAppServerAttempt", () => {
731769});
732770});
733771772+it("resumes a bound Codex thread when only dynamic tool descriptions change", async () => {
773+const sessionFile = path.join(tempDir, "session.jsonl");
774+const workspaceDir = path.join(tempDir, "workspace");
775+const params = createParams(sessionFile, workspaceDir);
776+const appServer = createThreadLifecycleAppServerOptions();
777+const request = vi.fn(async (method: string) => {
778+if (method === "thread/start") {
779+return threadStartResult("thread-existing");
780+}
781+if (method === "thread/resume") {
782+return { thread: { id: "thread-existing" }, modelProvider: "openai" };
783+}
784+throw new Error(`unexpected method: ${method}`);
785+});
786+787+await startOrResumeThread({
788+client: { request } as never,
789+ params,
790+cwd: workspaceDir,
791+dynamicTools: [
792+createMessageDynamicTool("Send and manage messages for the current Slack thread."),
793+],
794+ appServer,
795+});
796+const binding = await startOrResumeThread({
797+client: { request } as never,
798+ params,
799+cwd: workspaceDir,
800+dynamicTools: [
801+createMessageDynamicTool("Send and manage messages for the current Discord channel."),
802+],
803+ appServer,
804+});
805+806+expect(binding.threadId).toBe("thread-existing");
807+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
808+});
809+810+it("starts a new Codex thread when dynamic tool schemas change", async () => {
811+const sessionFile = path.join(tempDir, "session.jsonl");
812+const workspaceDir = path.join(tempDir, "workspace");
813+const params = createParams(sessionFile, workspaceDir);
814+const appServer = createThreadLifecycleAppServerOptions();
815+let nextThread = 1;
816+const request = vi.fn(async (method: string) => {
817+if (method === "thread/start") {
818+return threadStartResult(`thread-${nextThread++}`);
819+}
820+throw new Error(`unexpected method: ${method}`);
821+});
822+823+await startOrResumeThread({
824+client: { request } as never,
825+ params,
826+cwd: workspaceDir,
827+dynamicTools: [createMessageDynamicTool("Send and manage messages.", ["send"])],
828+ appServer,
829+});
830+const binding = await startOrResumeThread({
831+client: { request } as never,
832+ params,
833+cwd: workspaceDir,
834+dynamicTools: [createMessageDynamicTool("Send and manage messages.", ["send", "read"])],
835+ appServer,
836+});
837+838+expect(binding.threadId).toBe("thread-2");
839+expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/start"]);
840+});
841+734842it("passes configured app-server policy, sandbox, service tier, and model on resume", async () => {
735843const sessionFile = path.join(tempDir, "session.jsonl");
736844const workspaceDir = path.join(tempDir, "workspace");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。