diff --git a/.github/workflows/personal-release.yml b/.github/workflows/personal-release.yml index 6c4aa8651b22..ba31cbe348b1 100644 --- a/.github/workflows/personal-release.yml +++ b/.github/workflows/personal-release.yml @@ -48,6 +48,13 @@ jobs: - run: npm install --global @github/copilot@1.0.101 - name: Test release compatibility normalization run: node --test scripts/personal-release-random.node-check.ts + - name: Test ACP load compatibility + run: | + node --test scripts/personal-release-load.node-check.ts + temp="$(mktemp -d)" + trap 'rm -rf "$temp"' EXIT + cp scripts/personal-release.ts scripts/personal-release-load.ts "$temp/" + node --input-type=module -e 'await import(process.argv[1])' "$temp/personal-release.ts" - name: Prepare official source with pending fixes id: prepare env: @@ -57,6 +64,7 @@ jobs: run: | gh auth setup-git cp scripts/personal-release.ts "$RUNNER_TEMP/personal-release.ts" + cp scripts/personal-release-load.ts "$RUNNER_TEMP/" cp scripts/personal-return.* "$RUNNER_TEMP/" node "$RUNNER_TEMP/personal-release.ts" - uses: actions/upload-artifact@v7 diff --git a/scripts/personal-release-load.node-check.ts b/scripts/personal-release-load.node-check.ts new file mode 100644 index 000000000000..6646c4adc76a --- /dev/null +++ b/scripts/personal-release-load.node-check.ts @@ -0,0 +1,38 @@ +import * as NodeAssert from "node:assert/strict"; +import * as NodeTest from "node:test"; +import { normalizeLoadResponse } from "./personal-release-load.ts"; + +const legacy = ` waitForSessionLoadReplayIdle, + const runLoadSessionWithReplayIdle = ( + const idleFiber = yield* waitForSessionLoadReplayIdle({ + gateRef: sessionLoadGateRef, + }).pipe(Effect.forkIn(runtimeScope)); + const loaded = yield* Effect.raceFirst( + acp.agent.loadSession(loadPayload), + Fiber.join(idleFiber), + ).pipe( + Effect.ensuring(Fiber.interrupt(idleFiber).pipe(Effect.ignore)), + detail: "session/load timed out waiting for RPC response or replay idle gap", +`; +NodeTest.test("restores response-bound session loading", () => { + const result = normalizeLoadResponse(legacy); + NodeAssert.match(result, /yield\* acp\.agent\.loadSession\(loadPayload\)\.pipe/); + NodeAssert.doesNotMatch(result, /Fiber\.join\(idleFiber\)|waitForSessionLoadReplayIdle/); + NodeAssert.match(result, /timed out waiting for RPC response"/); + NodeAssert.equal(normalizeLoadResponse(result), result); +}); +NodeTest.test("leaves already-correct runtime alone", () => { + const current = "const runLoadSession = () => acp.agent.loadSession(loadPayload);"; + NodeAssert.equal(normalizeLoadResponse(current), current); +}); +NodeTest.test("rejects unrecognized race shape", () => { + NodeAssert.throws(() => + normalizeLoadResponse(legacy.replace("Fiber.join(idleFiber)", "Fiber.join(otherFiber)")), + ); + NodeAssert.throws(() => + normalizeLoadResponse(legacy.replace("runLoadSessionWithReplayIdle", "runLoadSessionChanged")), + ); + NodeAssert.throws(() => + normalizeLoadResponse(legacy + "\nconst extra = waitForSessionLoadReplayIdle;"), + ); +}); diff --git a/scripts/personal-release-load.ts b/scripts/personal-release-load.ts new file mode 100644 index 000000000000..5ddd724c7d7d --- /dev/null +++ b/scripts/personal-release-load.ts @@ -0,0 +1,49 @@ +// Preserve the carried fix: replay silence is not a session/load response. +export function normalizeLoadResponse(source: string): string { + const marker = " const runLoadSessionWithReplayIdle = ("; + if (!source.includes(marker)) { + if ( + source.includes("waitForSessionLoadReplayIdle") || + source.includes("runLoadSessionWithReplayIdle") + ) + throw new Error("Unexpected ACP load compatibility shape"); + return source; + } + if (!source.includes("waitForSessionLoadReplayIdle")) { + const directLoad = " const loaded = yield* acp.agent.loadSession(loadPayload).pipe("; + if ( + source.split(directLoad).length === 2 && + !source.includes("Fiber.join(idleFiber)") && + !source.includes("RPC response or replay idle gap") + ) + return source; + throw new Error("Unexpected ACP load compatibility shape: missing idle helper"); + } + const before = ` const idleFiber = yield* waitForSessionLoadReplayIdle({ + gateRef: sessionLoadGateRef, + }).pipe(Effect.forkIn(runtimeScope)); + const loaded = yield* Effect.raceFirst( + acp.agent.loadSession(loadPayload), + Fiber.join(idleFiber), + ).pipe( + Effect.ensuring(Fiber.interrupt(idleFiber).pipe(Effect.ignore)),`; + const after = ` // Replay silence does not acknowledge session/load or its configuration. + const loaded = yield* acp.agent.loadSession(loadPayload).pipe(`; + const replacements: ReadonlyArray = [ + [" waitForSessionLoadReplayIdle,\n", ""], + [before, after], + [ + "session/load timed out waiting for RPC response or replay idle gap", + "session/load timed out waiting for RPC response", + ], + ]; + let result = source; + for (const [from, to] of replacements) { + if (result.split(from).length !== 2) + throw new Error(`Unexpected ACP load compatibility shape: ${from}`); + result = result.replace(from, to); + } + if (result.includes("waitForSessionLoadReplayIdle") || result.includes("Fiber.join(idleFiber)")) + throw new Error("Unexpected ACP load compatibility shape: residual idle race"); + return result; +} diff --git a/scripts/personal-release.ts b/scripts/personal-release.ts index 3ad3486937a3..c6462895acab 100644 --- a/scripts/personal-release.ts +++ b/scripts/personal-release.ts @@ -3,6 +3,8 @@ import * as NodeChildProcess from "node:child_process"; import * as NodeFS from "node:fs"; +import { normalizeLoadResponse } from "./personal-release-load.ts"; + const upstream = "pingdotgg/t3code"; const fork = "Microck/t3code"; const fixes: ReadonlyArray<{ @@ -377,6 +379,14 @@ function prepare() { git("add", acpDriver); git("commit", "-m", "fix(acp): retain current random and digest services"); } + const acpRuntime = "packages/provider-acp/src/server/AcpSessionRuntime.ts"; + const runtimeSource = NodeFS.readFileSync(acpRuntime, "utf8"); + const normalizedRuntime = normalizeLoadResponse(runtimeSource); + if (normalizedRuntime !== runtimeSource) { + NodeFS.writeFileSync(acpRuntime, normalizedRuntime); + git("add", acpRuntime); + git("commit", "-m", "fix(acp): await the session load response"); + } // The CLI release index and downloads must point at the same distribution. replaceRequired( "packages/shared/src/cliRelease.ts",