| ... | ... | @@ -1653,6 +1653,193 @@ function makeCodexReplayTranscript(input: { |
| 1653 | 1653 | }; |
| 1654 | 1654 | } |
| 1655 | 1655 | |
| 1656 | function withReplayRequestId( |
| 1657 | entry: CodexReplay.CodexAppServerReplayEntry, |
| 1658 | id: number, |
| 1659 | ): CodexReplay.CodexAppServerReplayEntry { |
| 1660 | return entry.type !== "runtime_exit" && Predicate.isObject(entry.frame) |
| 1661 | ? { ...entry, frame: { ...entry.frame, id } } |
| 1662 | : entry; |
| 1663 | } |
| 1664 | |
| 1665 | describe("CodexAdapterV2 session initialize", () => { |
| 1666 | const openReplaySession = ( |
| 1667 | transcript: CodexReplay.CodexAppServerReplayTranscript, |
| 1668 | beforeEmitInbound?: CodexReplay.CodexAppServerReplayDriver["beforeEmitInbound"], |
| 1669 | ) => |
| 1670 | Effect.gen(function* () { |
| 1671 | const driver = yield* CodexReplay.makeReplayDriver( |
| 1672 | transcript, |
| 1673 | beforeEmitInbound === undefined ? {} : { beforeEmitInbound }, |
| 1674 | ); |
| 1675 | let initializeRequests = 0; |
| 1676 | const adapter = CodexAdapterV2.makeCodexAdapterV2({ |
| 1677 | instanceId: CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID, |
| 1678 | settings: DEFAULT_CODEX_SETTINGS, |
| 1679 | environment: {}, |
| 1680 | clientFactory: { |
| 1681 | open: (openInput) => |
| 1682 | Layer.build(CodexReplay.layerReplayWithDriver(driver)).pipe( |
| 1683 | Effect.flatMap((context) => |
| 1684 | Effect.service(CodexClient.CodexAppServerClient).pipe(Effect.provide(context)), |
| 1685 | ), |
| 1686 | Effect.map( |
| 1687 | (client) => |
| 1688 | ({ |
| 1689 | ...client, |
| 1690 | request: (method, params) => |
| 1691 | Effect.sync(() => { |
| 1692 | if (method === "initialize") initializeRequests++; |
| 1693 | }).pipe(Effect.andThen(client.request(method, params))), |
| 1694 | }) satisfies CodexClient.CodexAppServerClient["Service"], |
| 1695 | ), |
| 1696 | Effect.mapError( |
| 1697 | (cause) => |
| 1698 | new ProviderAdapterOpenSessionError({ |
| 1699 | driver: CodexAdapterV2.CODEX_DRIVER_KIND, |
| 1700 | providerSessionId: openInput.providerSessionId, |
| 1701 | cause, |
| 1702 | }), |
| 1703 | ), |
| 1704 | ), |
| 1705 | }, |
| 1706 | fileSystem: yield* FileSystem.FileSystem, |
| 1707 | idAllocator: yield* IdAllocator.IdAllocatorV2, |
| 1708 | serverConfig: yield* makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie), |
| 1709 | }); |
| 1710 | const runtime = yield* adapter.openSession({ |
| 1711 | threadId: ThreadId.make(`thread-${transcript.scenario}`), |
| 1712 | providerSessionId: ProviderSessionId.make(`provider-session-${transcript.scenario}`), |
| 1713 | modelSelection: CODEX_TEST_MODEL_SELECTION, |
| 1714 | runtimePolicy: CODEX_TEST_RUNTIME_POLICY, |
| 1715 | }); |
| 1716 | return { |
| 1717 | ensureThread: (threadId: string) => |
| 1718 | runtime.ensureThread({ |
| 1719 | threadId: ThreadId.make(threadId), |
| 1720 | modelSelection: CODEX_TEST_MODEL_SELECTION, |
| 1721 | runtimePolicy: CODEX_TEST_RUNTIME_POLICY, |
| 1722 | }), |
| 1723 | initializeRequests: () => initializeRequests, |
| 1724 | }; |
| 1725 | }); |
| 1726 | |
| 1727 | const replayPreamble = (nativeThreadId: string) => |
| 1728 | codexReplayPreamble({ nativeThreadId, nativeTurnId: "unused", prompt: "unused" }); |
| 1729 | |
| 1730 | it.effect("sends one initialize when two threads start on a fresh session at once", () => |
| 1731 | Effect.gen(function* () { |
| 1732 | const initializeAwaitingResponse = yield* Deferred.make<void>(); |
| 1733 | const releaseInitialize = yield* Deferred.make<void>(); |
| 1734 | // The transcript allows exactly one handshake: a second `initialize` |
| 1735 | // frame fails the replay, as Codex rejects it with "Already initialized". |
| 1736 | const session = yield* openReplaySession( |
| 1737 | makeCodexReplayTranscript({ |
| 1738 | scenario: "concurrent-initialize", |
| 1739 | entries: [ |
| 1740 | ...replayPreamble("concurrent-first").slice(0, 5), |
| 1741 | ...replayPreamble("concurrent-second") |
| 1742 | .slice(3, 5) |
| 1743 | .map((entry) => withReplayRequestId(entry, 3)), |
| 1744 | ], |
| 1745 | }), |
| 1746 | (entry) => |
| 1747 | entry.label === "initialize" |
| 1748 | ? Deferred.succeed(initializeAwaitingResponse, undefined).pipe( |
| 1749 | Effect.andThen(Deferred.await(releaseInitialize)), |
| 1750 | ) |
| 1751 | : Effect.void, |
| 1752 | ); |
| 1753 | |
| 1754 | const first = yield* session |
| 1755 | .ensureThread("thread-concurrent-first") |
| 1756 | .pipe(Effect.forkChild({ startImmediately: true })); |
| 1757 | yield* Deferred.await(initializeAwaitingResponse); |
| 1758 | // The second thread arrives while the handshake is still unanswered. |
| 1759 | const second = yield* session |
| 1760 | .ensureThread("thread-concurrent-second") |
| 1761 | .pipe(Effect.forkChild({ startImmediately: true })); |
| 1762 | yield* Deferred.succeed(releaseInitialize, undefined); |
| 1763 | |
| 1764 | const providerThreads = [yield* Fiber.join(first), yield* Fiber.join(second)]; |
| 1765 | assert.equal(session.initializeRequests(), 1); |
| 1766 | assert.sameMembers( |
| 1767 | providerThreads.map((providerThread) => providerThread.nativeThreadRef?.nativeId), |
| 1768 | ["concurrent-first", "concurrent-second"], |
| 1769 | ); |
| 1770 | }).pipe(Effect.scoped, Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), |
| 1771 | ); |
| 1772 | |
| 1773 | it.effect("retries initialize after a failed handshake", () => |
| 1774 | Effect.gen(function* () { |
| 1775 | const preamble = replayPreamble("initialize-retry"); |
| 1776 | const entries: Array<CodexReplay.CodexAppServerReplayEntry> = [ |
| 1777 | ...preamble.slice(0, 1), |
| 1778 | { |
| 1779 | type: "emit_inbound", |
| 1780 | label: "initialize", |
| 1781 | frame: { id: 1, error: { code: -32603, message: "Codex is not ready." } }, |
| 1782 | }, |
| 1783 | ...preamble.slice(0, 2).map((entry) => withReplayRequestId(entry, 2)), |
| 1784 | ...preamble.slice(2, 3), |
| 1785 | ...preamble.slice(3, 5).map((entry) => withReplayRequestId(entry, 3)), |
| 1786 | ]; |
| 1787 | const session = yield* openReplaySession( |
| 1788 | makeCodexReplayTranscript({ scenario: "initialize-retry", entries }), |
| 1789 | ); |
| 1790 | |
| 1791 | const failure = yield* session.ensureThread("thread-initialize-retry").pipe(Effect.flip); |
| 1792 | assert.equal(failure._tag, "ProviderAdapterEnsureThreadError"); |
| 1793 | const providerThread = yield* session.ensureThread("thread-initialize-retry"); |
| 1794 | assert.equal(providerThread.nativeThreadRef?.nativeId, "initialize-retry"); |
| 1795 | assert.equal(session.initializeRequests(), 2); |
| 1796 | }).pipe(Effect.scoped, Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), |
| 1797 | ); |
| 1798 | |
| 1799 | it.effect("completes the handshake after a caller is interrupted mid-initialize", () => |
| 1800 | Effect.gen(function* () { |
| 1801 | const initializeAwaitingResponse = yield* Deferred.make<void>(); |
| 1802 | const releaseInitialize = yield* Deferred.make<void>(); |
| 1803 | const preamble = replayPreamble("initialize-interrupted"); |
| 1804 | const session = yield* openReplaySession( |
| 1805 | makeCodexReplayTranscript({ |
| 1806 | scenario: "initialize-interrupted", |
| 1807 | entries: [ |
| 1808 | ...preamble.slice(0, 2), |
| 1809 | // Codex handled the interrupted caller's `initialize`, so it |
| 1810 | // rejects the next one. |
| 1811 | ...preamble.slice(0, 1).map((entry) => withReplayRequestId(entry, 2)), |
| 1812 | { |
| 1813 | type: "emit_inbound", |
| 1814 | label: "initialize-rejected", |
| 1815 | frame: { id: 2, error: { code: -32600, message: "Already initialized" } }, |
| 1816 | }, |
| 1817 | ...preamble.slice(2, 3), |
| 1818 | ...preamble.slice(3, 5).map((entry) => withReplayRequestId(entry, 3)), |
| 1819 | ], |
| 1820 | }), |
| 1821 | (entry) => |
| 1822 | entry.label === "initialize" |
| 1823 | ? Deferred.succeed(initializeAwaitingResponse, undefined).pipe( |
| 1824 | Effect.andThen(Deferred.await(releaseInitialize)), |
| 1825 | ) |
| 1826 | : Effect.void, |
| 1827 | ); |
| 1828 | |
| 1829 | const interrupted = yield* session |
| 1830 | .ensureThread("thread-initialize-interrupted") |
| 1831 | .pipe(Effect.forkChild({ startImmediately: true })); |
| 1832 | yield* Deferred.await(initializeAwaitingResponse); |
| 1833 | yield* Fiber.interrupt(interrupted); |
| 1834 | yield* Deferred.succeed(releaseInitialize, undefined); |
| 1835 | |
| 1836 | const providerThread = yield* session.ensureThread("thread-initialize-interrupted"); |
| 1837 | assert.equal(providerThread.nativeThreadRef?.nativeId, "initialize-interrupted"); |
| 1838 | assert.equal(session.initializeRequests(), 2); |
| 1839 | }).pipe(Effect.scoped, Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), |
| 1840 | ); |
| 1841 | }); |
| 1842 | |
| 1656 | 1843 | describe("CodexAdapterV2 post-settle continuation", () => { |
| 1657 | 1844 | const awaitUntil = (predicate: () => boolean, label: string): Effect.Effect<void> => |
| 1658 | 1845 | Effect.gen(function* () { |