From fc7006c5e39e6d1a17786dc7703cb53733b58c6d Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Tue, 21 Apr 2026 19:42:08 -0500 Subject: [PATCH 1/9] fix: verify published cli with okcode bin --- .github/workflows/release.yml | 4 ++-- docs/releases/v0.26.0/rollout-checklist.md | 2 +- scripts/prepare-release.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c9a7b7f..50f64938 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -342,8 +342,8 @@ jobs: - name: Verify published CLI run: | - npx --yes okcodes@${{ needs.preflight.outputs.version }} --version - npx --yes okcodes@${{ needs.preflight.outputs.version }} --help >/dev/null + npm exec --yes --package okcodes@${{ needs.preflight.outputs.version }} -- okcode --version + npm exec --yes --package okcodes@${{ needs.preflight.outputs.version }} -- okcode --help >/dev/null release: name: Publish GitHub Release diff --git a/docs/releases/v0.26.0/rollout-checklist.md b/docs/releases/v0.26.0/rollout-checklist.md index e30c1cc8..8ad6f948 100644 --- a/docs/releases/v0.26.0/rollout-checklist.md +++ b/docs/releases/v0.26.0/rollout-checklist.md @@ -52,7 +52,7 @@ Step-by-step playbook for the v0.26.0 release. Each phase must complete before a ## Phase 2: Post-release verification -- [ ] `npx --yes okcodes@0.26.0 --version` returns `0.26.0`. +- [ ] `npm exec --yes --package okcodes@0.26.0 -- okcode --version` returns `0.26.0`. - [ ] macOS installer launches and passes Gatekeeper. - [ ] Linux AppImage launches. - [ ] Windows installer installs and launches. diff --git a/scripts/prepare-release.ts b/scripts/prepare-release.ts index 72fd8d56..14ad772c 100644 --- a/scripts/prepare-release.ts +++ b/scripts/prepare-release.ts @@ -395,7 +395,7 @@ Step-by-step playbook for the v${version} release. Each phase must complete befo ## Phase 2: Post-release verification -- [ ] \`npx --yes okcodes@${version} --version\` returns \`${version}\`. +- [ ] \`npm exec --yes --package okcodes@${version} -- okcode --version\` returns \`${version}\`. - [ ] macOS installer launches and passes Gatekeeper. - [ ] Linux AppImage launches. - [ ] Windows installer installs and launches. From e38809828d0af5b06612e9ffd1a1aea9f1baf5ee Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Tue, 21 Apr 2026 20:31:16 -0500 Subject: [PATCH 2/9] fix: make npm publish rerun-safe --- apps/server/scripts/cli.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index abc83c07..c49ececc 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -185,6 +185,8 @@ const publishCmd = Command.make( const serverDir = path.join(repoRoot, "apps/server"); const packageJsonPath = path.join(serverDir, "package.json"); const backupPath = `${packageJsonPath}.bak`; + const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version); + const publishTarget = `${serverPackageJson.name}@${version}`; // Assert build assets exist for (const relPath of ["dist/index.mjs", "dist/client/index.html"]) { @@ -201,7 +203,6 @@ const publishCmd = Command.make( Effect.gen(function* () { // Resolve catalog dependencies before any file mutations. If this throws, // acquire fails and no release hook runs, so filesystem must still be untouched. - const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version); const pkg = { name: serverPackageJson.name, repository: serverPackageJson.repository, @@ -230,6 +231,25 @@ const publishCmd = Command.make( // Use: npm publish () => Effect.gen(function* () { + const versionExists = yield* runCommand( + ChildProcess.make("npm", ["view", publishTarget, "version"], { + cwd: serverDir, + stdout: config.verbose ? "inherit" : "ignore", + stderr: "inherit", + shell: process.platform === "win32", + }), + ).pipe( + Effect.as(true), + Effect.catchTag("CliError", () => Effect.succeed(false)), + ); + + if (versionExists) { + yield* Effect.logWarning( + `[cli] ${publishTarget} is already published; skipping npm publish.`, + ); + return; + } + const args = ["publish", "--access", config.access, "--tag", config.tag]; if (config.provenance) args.push("--provenance"); if (config.dryRun) args.push("--dry-run"); From 1cbbca6d4d951c2b4a4da5aee07a3caa080fa335 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Tue, 21 Apr 2026 20:54:25 -0500 Subject: [PATCH 3/9] fix: publish cli from cjs bin --- apps/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/package.json b/apps/server/package.json index 490e7b15..4af2ce71 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -8,7 +8,7 @@ "directory": "apps/server" }, "bin": { - "okcode": "./dist/index.mjs" + "okcode": "./dist/index.cjs" }, "files": [ "dist" From b7bdeff0d4e0eaac9e82824cfce105e260002682 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Tue, 21 Apr 2026 21:40:30 -0500 Subject: [PATCH 4/9] Add out-of-memory session recovery --- apps/web/src/components/ChatView.browser.tsx | 72 +++++++++++++++++++ apps/web/src/components/ChatView.tsx | 12 ++++ .../chat/ErrorNotificationBar.test.tsx | 29 +++++++- .../components/chat/ErrorNotificationBar.tsx | 44 ++++++++++++ .../src/components/chat/threadError.test.ts | 13 ++++ apps/web/src/components/chat/threadError.ts | 32 +++++++++ 6 files changed, 201 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index e6147f1f..8f462a4a 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -291,6 +291,34 @@ function buildFixture(snapshot: OrchestrationReadModel): TestFixture { }; } +function withThreadSessionError( + snapshot: OrchestrationReadModel, + input: { + status: OrchestrationSessionStatus; + lastError: string; + }, +): OrchestrationReadModel { + return { + ...snapshot, + threads: snapshot.threads.map((thread) => + thread.id === THREAD_ID && thread.session + ? { + ...thread, + session: { + threadId: thread.session.threadId, + providerName: thread.session.providerName, + runtimeMode: thread.session.runtimeMode, + activeTurnId: thread.session.activeTurnId, + status: input.status, + lastError: input.lastError, + updatedAt: NOW_ISO, + }, + } + : thread, + ), + }; +} + function addThreadToSnapshot( snapshot: OrchestrationReadModel, threadId: ThreadId, @@ -1634,6 +1662,50 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("resets the provider session from the error banner after an out-of-memory failure", async () => { + wsRequests.length = 0; + + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: withThreadSessionError( + createSnapshotForTargetUser({ + targetMessageId: "msg-user-oom-reset" as MessageId, + targetText: "oom reset target", + }), + { + status: "error", + lastError: + "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory", + }, + ), + }); + + try { + const recoverButton = await waitForElement( + () => + document.querySelector( + 'button[aria-label="Reset session after out-of-memory failure"]', + ), + "Unable to find out-of-memory recovery button.", + ); + + recoverButton.click(); + + await vi.waitFor( + () => + wsRequests.some( + (request) => + request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && + request.type === "thread.session.stop" && + request.threadId === THREAD_ID, + ), + { timeout: 8_000, interval: 16 }, + ); + } finally { + await mounted.cleanup(); + } + }); + it("keeps the new thread selected after clicking the new-thread button", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 17bf1656..af2e1f2f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3880,6 +3880,17 @@ export default function ChatView({ }); }; + const onRecoverFromOutOfMemory = async () => { + const api = readNativeApi(); + if (!api || !activeThread || isRemoteActionBlocked) return; + await api.orchestration.dispatchCommand({ + type: "thread.session.stop", + commandId: newCommandId(), + threadId: activeThread.id, + createdAt: new Date().toISOString(), + }); + }; + const onClearQueue = useCallback(() => { setOptimisticUserMessages((existing) => { for (const msg of existing) { @@ -4955,6 +4966,7 @@ export default function ChatView({ showNotificationDetails={settings.showNotificationDetails} includeDiagnosticsTipsInCopy={settings.includeDiagnosticsTipsInCopy} onDismissThreadError={() => setThreadError(activeThread.id, null)} + onRecoverFromOutOfMemory={() => void onRecoverFromOutOfMemory()} providerStatus={activeProviderStatus} transportState={transportState} isMobileCompanion={isMobileCompanion} diff --git a/apps/web/src/components/chat/ErrorNotificationBar.test.tsx b/apps/web/src/components/chat/ErrorNotificationBar.test.tsx index b23bc183..6b880c16 100644 --- a/apps/web/src/components/chat/ErrorNotificationBar.test.tsx +++ b/apps/web/src/components/chat/ErrorNotificationBar.test.tsx @@ -24,7 +24,8 @@ const THREAD_ERROR = function renderBar( overrides: Partial> = {}, ): ReactElement { - const { onDismissThreadError, transportState, ...restOverrides } = overrides; + const { onDismissThreadError, onRecoverFromOutOfMemory, transportState, ...restOverrides } = + overrides; return ( ); @@ -85,4 +87,29 @@ describe("ErrorNotificationBar", () => { expect(markup).toContain("Worktree thread could not start"); expect(markup).toContain("Base branch 'main' does not resolve to a commit yet."); }); + + it("shows an out-of-memory recovery action when the thread error is recoverable", async () => { + const onRecoverFromOutOfMemory = vi.fn(); + let renderer: ReactTestRenderer | null = null; + await act(async () => { + renderer = create( + renderBar({ + threadError: + "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory", + onRecoverFromOutOfMemory, + }), + ); + }); + + const root = renderer!.root; + const recoverButton = root.findByProps({ + "aria-label": "Reset session after out-of-memory failure", + }); + + await act(async () => { + recoverButton.props.onClick(); + }); + + expect(onRecoverFromOutOfMemory).toHaveBeenCalledTimes(1); + }); }); diff --git a/apps/web/src/components/chat/ErrorNotificationBar.tsx b/apps/web/src/components/chat/ErrorNotificationBar.tsx index f8b9298d..dfedf6aa 100644 --- a/apps/web/src/components/chat/ErrorNotificationBar.tsx +++ b/apps/web/src/components/chat/ErrorNotificationBar.tsx @@ -13,6 +13,7 @@ import { buildThreadErrorDiagnosticsCopy, humanizeThreadError, isAuthenticationThreadError, + isOutOfMemoryThreadError, } from "./threadError"; import { getProviderStatusHeading, @@ -33,6 +34,8 @@ interface ErrorNotificationBarProps { includeDiagnosticsTipsInCopy?: boolean; /** Dismiss the thread error */ onDismissThreadError?: () => void; + /** Reset a provider session after an OOM failure */ + onRecoverFromOutOfMemory?: () => void; /** Provider health status */ providerStatus: ServerProviderStatus | null; /** Companion transport state (only relevant for mobile companion) */ @@ -49,6 +52,9 @@ interface NotificationItem { description: string; detailsText?: string | null; diagnosticsCopyText?: string | null; + actionLabel?: string; + actionAriaLabel?: string; + onAction?: () => void; severity: "error" | "warning" | "info"; dismissible: boolean; onDismiss?: () => void; @@ -60,6 +66,7 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ showNotificationDetails = false, includeDiagnosticsTipsInCopy = false, onDismissThreadError, + onRecoverFromOutOfMemory, providerStatus, transportState, isMobileCompanion, @@ -129,6 +136,8 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ if (threadError) { if (showAuthFailuresAsErrors || !isAuthenticationThreadError(threadError)) { const presentation = humanizeThreadError(threadError); + const showOutOfMemoryRecovery = + isOutOfMemoryThreadError(threadError) && onRecoverFromOutOfMemory !== undefined; items.push({ id: "thread-error", kind: "thread-error", @@ -139,6 +148,13 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ diagnosticsCopyText: buildThreadErrorDiagnosticsCopy(threadError, { includeTips: includeDiagnosticsTipsInCopy, }), + ...(showOutOfMemoryRecovery + ? { + actionLabel: "Reset session", + actionAriaLabel: "Reset session after out-of-memory failure", + onAction: onRecoverFromOutOfMemory, + } + : {}), severity: "error", dismissible: !!onDismissThreadError, ...(onDismissThreadError ? { onDismiss: onDismissThreadError } : {}), @@ -152,6 +168,7 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ showAuthFailuresAsErrors, includeDiagnosticsTipsInCopy, onDismissThreadError, + onRecoverFromOutOfMemory, providerStatus, transportState, isMobileCompanion, @@ -217,6 +234,9 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({ if (visibleNotifications.length === 0) return null; const primary = visibleNotifications[0]!; + const actionNotification = visibleNotifications.find( + (notification) => notification.onAction && notification.actionLabel, + ); const PrimaryIcon = primary.icon; const count = visibleNotifications.length; const countLabel = count === 1 ? "1 notification" : `${count} notifications`; @@ -257,6 +277,18 @@ export const ErrorNotificationBar = memo(function ErrorNotificationBar({
+ {actionNotification?.onAction && actionNotification.actionLabel ? ( + + ) : null} + ) : null} {notif.kind === "thread-error" && notif.diagnosticsCopyText ? ( { expect(isAuthenticationThreadError("Provider crashed while starting.")).toBe(false); }); + it("detects out-of-memory failures", () => { + expect( + isOutOfMemoryThreadError( + "Provider crashed: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory", + ), + ).toBe(true); + expect( + isOutOfMemoryThreadError("Process exited: memory limit exceeded while streaming turn"), + ).toBe(true); + expect(isOutOfMemoryThreadError("Provider crashed while starting.")).toBe(false); + }); + it("builds redacted diagnostics copy without optional tips by default", () => { expect( buildThreadErrorDiagnosticsCopy( diff --git a/apps/web/src/components/chat/threadError.ts b/apps/web/src/components/chat/threadError.ts index c1f61778..ddeb52ed 100644 --- a/apps/web/src/components/chat/threadError.ts +++ b/apps/web/src/components/chat/threadError.ts @@ -26,6 +26,13 @@ const AUTH_FAILURE_PATTERNS = [ "could not resolve authentication method", "authentication required", ] as const; +const OUT_OF_MEMORY_PATTERNS = [ + "out of memory", + "heap out of memory", + "reached heap limit", + "memory limit exceeded", + "allocation failed - javascript heap", +] as const; function extractWorktreeDetail(error: string): string | null { if (!error.startsWith(WORKTREE_COMMAND_PREFIX)) { @@ -62,6 +69,12 @@ function buildTroubleshootingTips(error: string, presentation: ThreadErrorPresen ); } + if (isOutOfMemoryThreadError(error)) { + tips.push( + "Reset the provider session, then retry with a smaller prompt, fewer attachments, or less terminal context.", + ); + } + if (presentation.title === "Worktree thread could not start") { tips.push( "Create the first commit or switch to a base branch that resolves to a commit before starting a worktree thread.", @@ -81,6 +94,16 @@ export function isAuthenticationThreadError(error: string | null | undefined): b return AUTH_FAILURE_PATTERNS.some((pattern) => lower.includes(pattern)); } +export function isOutOfMemoryThreadError(error: string | null | undefined): boolean { + const trimmed = error?.trim(); + if (!trimmed) { + return false; + } + + const lower = trimmed.toLowerCase(); + return OUT_OF_MEMORY_PATTERNS.some((pattern) => lower.includes(pattern)); +} + export function humanizeThreadError(error: string): ThreadErrorPresentation { const trimmed = redactSensitiveText(error).trim(); const worktreeDetail = extractWorktreeDetail(trimmed); @@ -92,6 +115,15 @@ export function humanizeThreadError(error: string): ThreadErrorPresentation { }; } + if (isOutOfMemoryThreadError(trimmed)) { + return { + title: "Session ran out of memory", + description: + "The provider session ran out of memory. Reset the session, then resend the prompt.", + technicalDetails: trimmed, + }; + } + return { title: null, description: trimmed.length > 0 ? trimmed : "An unexpected error occurred.", From fd405466b701c55bf76aeba0f6d13010bab7fb79 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 22 Apr 2026 15:33:34 -0500 Subject: [PATCH 5/9] fix: bundle diffs into server cli --- apps/server/tsdown.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts index c1470751..1c86b342 100644 --- a/apps/server/tsdown.config.ts +++ b/apps/server/tsdown.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ outDir: "dist", sourcemap: true, clean: true, - noExternal: (id) => id.startsWith("@okcode/"), + noExternal: (id) => id.startsWith("@okcode/") || id === "@pierre/diffs", inlineOnly: false, banner: { js: "#!/usr/bin/env node\n", From 51cbe4fcbd1d9ad282632b15b0204eaa684d06f0 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 22 Apr 2026 15:47:33 -0500 Subject: [PATCH 6/9] fix: verify published cli with temp install --- .github/workflows/release.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50f64938..f12d64ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -342,8 +342,14 @@ jobs: - name: Verify published CLI run: | - npm exec --yes --package okcodes@${{ needs.preflight.outputs.version }} -- okcode --version - npm exec --yes --package okcodes@${{ needs.preflight.outputs.version }} -- okcode --help >/dev/null + set -euo pipefail + tmpdir="$(mktemp -d)" + pushd "$tmpdir" >/dev/null + npm init -y >/dev/null 2>&1 + npm install --no-save "okcodes@${{ needs.preflight.outputs.version }}" >/dev/null 2>&1 + ./node_modules/.bin/okcode --version + ./node_modules/.bin/okcode --help >/dev/null + popd >/dev/null release: name: Publish GitHub Release From 138fb72df9557c98cc480e75d7bf1915d2361f88 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 22 Apr 2026 16:57:15 -0500 Subject: [PATCH 7/9] fix: inline diff parsing for cli release --- apps/server/src/checkpointing/Diffs.ts | 66 ++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/apps/server/src/checkpointing/Diffs.ts b/apps/server/src/checkpointing/Diffs.ts index c2f867b9..badedc21 100644 --- a/apps/server/src/checkpointing/Diffs.ts +++ b/apps/server/src/checkpointing/Diffs.ts @@ -1,11 +1,23 @@ -import { parsePatchFiles } from "@pierre/diffs"; - export interface TurnDiffFileSummary { readonly path: string; readonly additions: number; readonly deletions: number; } +interface MutableTurnDiffFileSummary { + path: string; + additions: number; + deletions: number; +} + +function normalizeDiffPath(path: string): string { + return path.replace(/^a\//, "").replace(/^b\//, ""); +} + +function createFileSummary(path: string): MutableTurnDiffFileSummary { + return { path, additions: 0, deletions: 0 }; +} + export function parseTurnDiffFilesFromUnifiedDiff( diff: string, ): ReadonlyArray { @@ -14,14 +26,48 @@ export function parseTurnDiffFilesFromUnifiedDiff( return []; } - const parsedPatches = parsePatchFiles(normalized); - const files = parsedPatches.flatMap((patch) => - patch.files.map((file) => ({ - path: file.name, - additions: file.hunks.reduce((total, hunk) => total + hunk.additionLines, 0), - deletions: file.hunks.reduce((total, hunk) => total + hunk.deletionLines, 0), - })), - ); + const files: MutableTurnDiffFileSummary[] = []; + let currentFile: MutableTurnDiffFileSummary | null = null; + let inHunk = false; + + for (const line of normalized.split("\n")) { + if (line.startsWith("diff --git ")) { + if (currentFile) files.push(currentFile); + + const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line); + currentFile = createFileSummary(normalizeDiffPath(match?.[2] ?? match?.[1] ?? line)); + inHunk = false; + continue; + } + + if (!currentFile) { + continue; + } + + if (line.startsWith("@@")) { + inHunk = true; + continue; + } + + if (!inHunk) { + continue; + } + + if (line.startsWith("+++ ") || line.startsWith("--- ")) { + continue; + } + + if (line.startsWith("+")) { + currentFile.additions += 1; + continue; + } + + if (line.startsWith("-")) { + currentFile.deletions += 1; + } + } + + if (currentFile) files.push(currentFile); return files.toSorted((left, right) => left.path.localeCompare(right.path)); } From 4c65c3d0798e8d4e4aff1b3e59af5d65ae7c8f94 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 22 Apr 2026 17:17:30 -0500 Subject: [PATCH 8/9] release: prepare v0.26.4 --- CHANGELOG.md | 20 ++++++ apps/desktop/package.json | 2 +- apps/mobile/android/app/build.gradle | 2 +- .../ios/App/App.xcodeproj/project.pbxproj | 4 +- apps/mobile/package.json | 2 +- apps/server/package.json | 2 +- apps/web/package.json | 2 +- docs/releases/README.md | 1 + docs/releases/v0.26.4.md | 38 +++++++++++ docs/releases/v0.26.4/assets.md | 57 ++++++++++++++++ docs/releases/v0.26.4/rollout-checklist.md | 67 +++++++++++++++++++ docs/releases/v0.26.4/soak-test-plan.md | 39 +++++++++++ packages/contracts/package.json | 2 +- 13 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 docs/releases/v0.26.4.md create mode 100644 docs/releases/v0.26.4/assets.md create mode 100644 docs/releases/v0.26.4/rollout-checklist.md create mode 100644 docs/releases/v0.26.4/soak-test-plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 50c924f0..e4adfa0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.26.4] - 2026-04-22 + +See [docs/releases/v0.26.4.md](docs/releases/v0.26.4.md) for full notes and [docs/releases/v0.26.4/assets.md](docs/releases/v0.26.4/assets.md) for release asset inventory. + +### Added + +- Add out-of-memory session recovery. + +### Changed + +- Bundle diffs into the server cli. +- Inline diff parsing for cli release. + +### Fixed + +- Publish cli from cjs bin. +- Make npm publish rerun-safe. +- Verify published cli with okcode bin. +- Verify published cli with temp install. + ## [0.26.2] - 2026-04-21 See [docs/releases/v0.26.2.md](docs/releases/v0.26.2.md) for full notes and [docs/releases/v0.26.2/assets.md](docs/releases/v0.26.2/assets.md) for release asset inventory. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ffc8ee44..ef375a55 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/desktop", - "version": "0.26.2", + "version": "0.26.4", "private": true, "main": "dist-electron/main.js", "scripts": { diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index ad804b41..7a9119fd 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "0.26.2" + versionName "0.26.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/apps/mobile/ios/App/App.xcodeproj/project.pbxproj b/apps/mobile/ios/App/App.xcodeproj/project.pbxproj index c5400531..b1e3eab8 100644 --- a/apps/mobile/ios/App/App.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/App/App.xcodeproj/project.pbxproj @@ -306,7 +306,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.26.2; + MARKETING_VERSION = 0.26.4; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -331,7 +331,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.26.2; + MARKETING_VERSION = 0.26.4; PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b4e0ea5d..5ce50bd0 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/mobile", - "version": "0.26.2", + "version": "0.26.4", "private": true, "type": "module", "scripts": { diff --git a/apps/server/package.json b/apps/server/package.json index 0140de53..5a010b58 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "okcodes", - "version": "0.26.2", + "version": "0.26.4", "license": "MIT", "repository": { "type": "git", diff --git a/apps/web/package.json b/apps/web/package.json index f150c101..7c70ee1b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/web", - "version": "0.26.2", + "version": "0.26.4", "private": true, "type": "module", "scripts": { diff --git a/docs/releases/README.md b/docs/releases/README.md index 124897ff..c6cb0f0e 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -9,6 +9,7 @@ Use this directory for versioned release notes and asset manifests only: | Version | Summary | Assets | | -------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------- | +| [0.26.4](v0.26.4.md) | Out-of-memory session recovery and CLI release hardening | [manifest](v0.26.4/assets.md) | | [0.26.2](v0.26.2.md) | Release with 1 new feature(s), 4 fix(es), 3 improvement(s) | [manifest](v0.26.2/assets.md) | | [0.26.0](v0.26.0.md) | File-content search, desktop terminal docking, transport sna | [manifest](v0.26.0/assets.md) | | [0.25.0](v0.25.0.md) | Project icon file picker, gateway auth terminology cleanup, | [manifest](v0.25.0/assets.md) | diff --git a/docs/releases/v0.26.4.md b/docs/releases/v0.26.4.md new file mode 100644 index 00000000..c434e884 --- /dev/null +++ b/docs/releases/v0.26.4.md @@ -0,0 +1,38 @@ +# OK Code v0.26.4 + +**Date:** 2026-04-22 +**Tag:** [`v0.26.4`](https://github.com/OpenKnots/okcode/releases/tag/v0.26.4) + +## Summary + +Re-cut release after `v0.26.3` was already present on npm; ships the same branch state with out-of-memory session recovery and CLI release hardening. + +## Highlights + +- **Add out-of-memory session recovery.** +- **Bundle diffs into the server cli.** +- **Inline diff parsing for cli release.** +- **Publish cli from cjs bin.** +- **Make npm publish rerun-safe.** +- **Verify published cli with okcode bin.** +- **Verify published cli with temp install.** + +## Breaking changes + +- None. + +## Upgrade and install + +- **CLI:** `npm install -g okcodes@0.26.4` once the desktop/CLI release workflow finishes. +- **Desktop:** Download from [GitHub Releases](https://github.com/OpenKnots/okcode/releases/tag/v0.26.4). Filenames are listed in [assets.md](v0.26.4/assets.md). +- **iOS:** Available via TestFlight after the separate Release iOS workflow is dispatched for this tag. + +## Known limitations + +OK Code remains early work in progress. Expect rough edges around session recovery, streaming edge cases, and platform-specific desktop behavior. Report issues on GitHub. + +## Release operations + +- Review the [asset manifest](v0.26.4/assets.md) to confirm every expected GitHub Release attachment is present. +- Use the [rollout checklist](v0.26.4/rollout-checklist.md) to walk the desktop/CLI release plus the separate iOS TestFlight dispatch through post-release verification. +- Use the [soak test plan](v0.26.4/soak-test-plan.md) to validate the highest-risk surfaces after the tag is live. diff --git a/docs/releases/v0.26.4/assets.md b/docs/releases/v0.26.4/assets.md new file mode 100644 index 00000000..d9ee97f1 --- /dev/null +++ b/docs/releases/v0.26.4/assets.md @@ -0,0 +1,57 @@ +# v0.26.4 — Release assets (manifest) + +Binaries are **not** stored in this git repository; they are attached to the [GitHub Release for `v0.26.4`](https://github.com/OpenKnots/okcode/releases/tag/v0.26.4) by the [Release workflow](../../.github/workflows/release.yml). + +The GitHub Release also includes **documentation attachments** (same content as in-repo, stable filenames for download): + +| File | Source in repo | +| --------------------------- | ------------------------------------- | +| `okcode-CHANGELOG.md` | [CHANGELOG.md](../../../CHANGELOG.md) | +| `okcode-RELEASE-NOTES.md` | [v0.26.4.md](../v0.26.4.md) | +| `okcode-ASSETS-MANIFEST.md` | This file | + +After the workflow completes, expect **installer and updater** artifacts similar to the following (exact names may include the product name `OK Code` and version `0.26.4`). + +## Desktop installers and payloads + +| Platform | Kind | Typical pattern | +| ------------------- | -------------- | --------------- | +| macOS Apple Silicon | DMG (signed) | `*.dmg` (arm64) | +| macOS Intel | DMG (signed) | `*.dmg` (x64) | +| macOS | ZIP (updater) | `*.zip` | +| Linux x64 | AppImage | `*.AppImage` | +| Windows x64 | NSIS installer | `*.exe` | + +### macOS code signing and notarization + +All macOS DMG and ZIP payloads are **code-signed** with an Apple Developer ID certificate and **notarized** via the Apple notarization service. Gatekeeper will verify the signature on first launch. The hardened runtime is enabled with entitlements defined in `apps/desktop/resources/entitlements.mac.plist`. + +## Electron updater metadata + +| File | Purpose | +| ------------------ | --------------------------------------------------------- | +| `latest-mac.yml` | macOS update manifest (merged from per-arch builds in CI) | +| `latest-linux.yml` | Linux update manifest | +| `latest.yml` | Windows update manifest | +| `*.blockmap` | Differential download block maps | + +## iOS (TestFlight) + +The iOS build is uploaded directly to App Store Connect / TestFlight by the separately dispatched [Release iOS workflow](../../.github/workflows/release-ios.yml). No IPA artifact is attached to the GitHub Release. + +| Detail | Value | +| ----------------- | ------------------------------------------ | +| Bundle ID | `com.openknots.okcode.mobile` | +| Marketing version | `0.26.4` | +| Build number | Set from `GITHUB_RUN_NUMBER` at build time | + +## Checksums + +SHA-256 checksums are not committed here; verify downloads via GitHub's release UI or `gh release download` if you use the GitHub CLI. + +## Operational references + +| File | Purpose | +| -------------------------------------------- | ----------------------------------------------------------------- | +| [rollout-checklist.md](rollout-checklist.md) | Step-by-step release playbook from preflight through post-release | +| [soak-test-plan.md](soak-test-plan.md) | Structured release validation for the highest-risk surfaces | diff --git a/docs/releases/v0.26.4/rollout-checklist.md b/docs/releases/v0.26.4/rollout-checklist.md new file mode 100644 index 00000000..6369c225 --- /dev/null +++ b/docs/releases/v0.26.4/rollout-checklist.md @@ -0,0 +1,67 @@ +# v0.26.4 Rollout Checklist + +Step-by-step playbook for the v0.26.4 release. Each phase must complete before advancing. + +## Phase 0: Pre-flight + +- [ ] Verify all release package versions are `0.26.4`: + - `apps/server/package.json` + - `apps/desktop/package.json` + - `apps/web/package.json` + - `apps/mobile/package.json` + - `packages/contracts/package.json` +- [ ] Verify Android `versionName` and iOS `MARKETING_VERSION` both match `0.26.4`. +- [ ] Confirm `CHANGELOG.md` has `## [0.26.4] - 2026-04-22`. +- [ ] Confirm `docs/releases/v0.26.4.md` exists with Summary, Highlights, Upgrade and install, and Release operations sections. +- [ ] Confirm `docs/releases/v0.26.4/assets.md` exists and lists every expected attachment class. +- [ ] Confirm `docs/releases/v0.26.4/rollout-checklist.md` and `docs/releases/v0.26.4/soak-test-plan.md` exist. +- [ ] Confirm `docs/releases/README.md` includes the v0.26.4 row. +- [ ] Run `bun run release:validate 0.26.4`. +- [ ] Confirm the working tree is clean. +- [ ] Confirm you are on `main`. + +### Quality gates + +- [ ] `bun run fmt:check` +- [ ] `bun run lint` +- [ ] `bun run typecheck` +- [ ] `bun run test` +- [ ] `bun run --cwd apps/web test:browser` +- [ ] `bun run test:desktop-smoke` +- [ ] `bun run release:smoke` + +## Phase 1: Publish + +- [ ] Push the release-prep commit to `main`. +- [ ] Create and push tag `v0.26.4`. +- [ ] Verify the coordinated `release.yml` workflow starts. +- [ ] Trigger `release-ios.yml` manually for `v0.26.4` (or the matching release ref if the tag is unavailable). +- [ ] Monitor `release.yml` through Preflight, Desktop builds, Publish CLI, Publish GitHub Release, and Finalize release. +- [ ] Monitor `release-ios.yml` through Preflight, iOS signing preflight, and iOS TestFlight. + +### Asset verification + +- [ ] GitHub Release body matches `docs/releases/v0.26.4.md`. +- [ ] `okcode-CHANGELOG.md` is attached. +- [ ] `okcode-RELEASE-NOTES.md` is attached. +- [ ] `okcode-ASSETS-MANIFEST.md` is attached. +- [ ] macOS release artifacts are attached: DMG, ZIP, updater manifest, and blockmaps. +- [ ] Linux release artifacts are attached: AppImage and updater manifest if generated. +- [ ] Windows release artifacts are attached: installer, updater manifest, and blockmaps. +- [ ] If the Intel compatibility workflow is run, confirm the x64 macOS DMG is attached separately. + +## Phase 2: Post-release verification + +- [ ] `npm exec --yes --package okcodes@0.26.4 -- okcode --version` returns `0.26.4`. +- [ ] macOS installer launches and passes Gatekeeper. +- [ ] Linux AppImage launches. +- [ ] Windows installer installs and launches. +- [ ] Desktop auto-update metadata is present for supported platforms. +- [ ] If iOS signing was enabled, confirm the new TestFlight build appears. +- [ ] Confirm the finalize job did not need to push another version-alignment commit, or review its no-op output if versions were already aligned before tagging. + +## Phase 3: Follow-through + +- [ ] Trigger the Intel compatibility workflow if macOS x64 artifacts are required for this train. +- [ ] Update external release references or announcements. +- [ ] Monitor reports for regressions in provider onboarding, auth flows, release packaging, and cross-platform install/update behavior. diff --git a/docs/releases/v0.26.4/soak-test-plan.md b/docs/releases/v0.26.4/soak-test-plan.md new file mode 100644 index 00000000..62cf7feb --- /dev/null +++ b/docs/releases/v0.26.4/soak-test-plan.md @@ -0,0 +1,39 @@ +# v0.26.4 Soak Test Plan + +Structured validation plan for the highest-risk surfaces in v0.26.4. + +## 1. Provider onboarding and auth flows + +| Step | Expected | Pass | +| ----------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---- | +| Configure each primary provider from Settings | Provider setup screens save cleanly and validation messages stay actionable | [ ] | +| Exercise Claude and OpenClaw auth flows after reload | Saved credentials and provider state restore without stale or conflicting UI | [ ] | +| Start a Codex or Copilot-backed conversation after provider setup | Turn creation, streaming, and provider selection remain consistent | [ ] | +| Trigger an auth failure intentionally | Errors surface clearly without leaking secrets or breaking follow-up retries | [ ] | + +## 2. Settings and configuration surfaces + +| Step | Expected | Pass | +| ---------------------------------------------------------- | --------------------------------------------------------------------- | ---- | +| Open the settings route on desktop and narrow layouts | Navigation stays stable and each section is reachable | [ ] | +| Change provider availability and default options | Picker filtering and availability controls update without stale state | [ ] | +| Use hotkey configuration controls and reset actions | Shortcuts persist, restore, and do not regress the editor UI | [ ] | +| Open the browser-preview-related settings and helper links | The helper flow launches correctly and does not break the app shell | [ ] | + +## 3. Runtime and review workflows + +| Step | Expected | Pass | +| --------------------------------------------------------------- | ------------------------------------------------------------------ | ---- | +| Run a thread that emits runtime events and reconnect mid-stream | Session state and event feeds remain consistent after reconnect | [ ] | +| Open the PR review dashboard with recent review history | Dashboard loads quickly and shows the expected recent activity | [ ] | +| Navigate between threads, projects, and restored sessions | Cached lookups, projections, and route transitions stay responsive | [ ] | +| Trigger browser preview and workspace activity during a turn | The app avoids flicker, stale panes, and blocked input | [ ] | + +## 4. Desktop, CLI, and release packaging + +| Step | Expected | Pass | +| ----------------------------------------------------------------------- | ------------------------------------------------------ | ---- | +| Run `bun run test:desktop-smoke` on the release branch | Desktop packaging smoke remains green | [ ] | +| Run `bun run release:smoke` before and after tagging | Release-specific workflow checks remain green | [ ] | +| Verify a packaged desktop artifact launches and reports the new version | Installed app opens cleanly and reports v0.26.4 | [ ] | +| Verify the CLI package after publish or from a packed tarball | `okcode --version` and help commands resolve correctly | [ ] | diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 1bcdc149..f6422589 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/contracts", - "version": "0.26.2", + "version": "0.26.4", "private": true, "files": [ "dist" From a06ca5b38313a19afd7ff7d817410d54b3a0dc70 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 22 Apr 2026 17:31:12 -0500 Subject: [PATCH 9/9] fix: retry macos desktop packaging in release --- .github/workflows/release.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f12d64ec..a4331115 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -228,7 +228,25 @@ jobs: fi fi - bun run dist:desktop:artifact -- "${args[@]}" + build_desktop_artifact() { + bun run dist:desktop:artifact -- "${args[@]}" + } + + if [[ "${{ matrix.platform }}" == "mac" ]]; then + for attempt in 1 2; do + if build_desktop_artifact; then + break + fi + if [[ "$attempt" -eq 1 ]]; then + echo "macOS desktop packaging failed once; retrying after a short delay..." >&2 + sleep 30 + continue + fi + exit 1 + done + else + build_desktop_artifact + fi - name: Validate packaged artifact outputs shell: bash