From e840c0244e9ce9a80999af320fdb2a2ae478a94f Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 10 Apr 2026 14:38:06 -0500 Subject: [PATCH 1/2] Release v0.22.1 with decision workspace updates - Add decision workspace projections, persistence, and RPC wiring - Improve OpenClaw gateway diagnostics and handshake handling - Bump app versions and add release notes for 0.22.1 --- CHANGELOG.md | 28 ++++++ 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 +- .../decision/Services/DecisionProjection.ts | 9 +- apps/server/src/main.test.ts | 21 +++- apps/server/src/openclawGatewayTest.ts | 76 ++++++++++----- .../Layers/ProjectionOverviewQuery.ts | 30 +++--- .../Layers/ProjectionPipeline.test.ts | 2 + .../Layers/ProjectionThreadDetailQuery.ts | 66 +++++++++---- apps/server/src/orchestration/decider.ts | 10 ++ .../src/orchestration/projector.test.ts | 1 + .../Layers/DecisionConsultations.ts | 4 +- .../Layers/DecisionScoreSnapshots.ts | 4 +- apps/server/src/persistence/Migrations.ts | 4 + ...023_ProjectionPendingUserInputsBackfill.ts | 22 +++++ .../Services/DecisionConsultations.ts | 5 +- .../Services/DecisionScoreSnapshots.ts | 7 +- .../provider/Layers/OpenClawGatewayClient.ts | 96 ++++++++++++------- apps/server/src/wsServer.ts | 21 ++++ apps/web/package.json | 2 +- apps/web/src/components/timelineHeight.ts | 2 +- apps/web/src/wsNativeApi.ts | 15 +++ bun.lock | 10 +- docs/releases/README.md | 25 ++--- docs/releases/v0.22.1.md | 70 ++++++++++++++ docs/releases/v0.22.1/assets.md | 67 +++++++++++++ docs/releases/v0.22.1/rollout-checklist.md | 65 +++++++++++++ docs/releases/v0.22.1/soak-test-plan.md | 48 ++++++++++ packages/contracts/package.json | 2 +- packages/contracts/src/decision.ts | 6 +- packages/contracts/src/ipc.ts | 4 +- packages/contracts/src/orchestration.ts | 6 +- packages/contracts/src/ws.ts | 4 + 36 files changed, 602 insertions(+), 142 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/023_ProjectionPendingUserInputsBackfill.ts create mode 100644 docs/releases/v0.22.1.md create mode 100644 docs/releases/v0.22.1/assets.md create mode 100644 docs/releases/v0.22.1/rollout-checklist.md create mode 100644 docs/releases/v0.22.1/soak-test-plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdfc9a8a..af58a8393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.22.1] - 2026-04-10 + +See [docs/releases/v0.22.1.md](docs/releases/v0.22.1.md) for full notes and [docs/releases/v0.22.1/assets.md](docs/releases/v0.22.1/assets.md) for release asset inventory. + +### Added + +- Add decision workspace contracts, projections, persistence tables, and WebSocket wiring groundwork. +- Add pending user input projections plus thread overview and detail queries. +- Add sidebar density controls, connection test controls, and expandable notification diagnostics with copy support. +- Add companion pairing contracts and mobile pairing stubs. +- Add stop support for pending git actions and external GitHub link opening from the preview popout. +- Add OpenClaw maintainer workflow skills. + +### Changed + +- Switch SME Claude flows to Claude Code CLI. +- Extract the OpenClaw gateway client with auth fallback and modernize the gateway handshake flow. +- Refresh theme tokens, default typography, and VS Code icon manifests. +- Preserve thread routes in desktop pop-out windows and widen preview viewport inputs. +- Render SME replies as markdown and replace the draft upload icon with a close action. + +### Fixed + +- Ignore expected redacted auth shutdown noise in Codex logs. +- Normalize React language ids for syntax highlighting. +- Defer the empty diff guard until after hook setup. + ## [0.22.0] - 2026-04-09 See [docs/releases/v0.22.0.md](docs/releases/v0.22.0.md) for full notes and [docs/releases/v0.22.0/assets.md](docs/releases/v0.22.0/assets.md) for release asset inventory. @@ -658,3 +685,4 @@ First public version tag. See [docs/releases/v0.0.1.md](docs/releases/v0.0.1.md) [0.20.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.20.0 [0.21.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.21.0 [0.22.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.22.0 +[0.22.1]: https://github.com/OpenKnots/okcode/releases/tag/v0.22.1 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ffa27ba4b..1acc35d37 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/desktop", - "version": "0.22.0", + "version": "0.22.1", "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 152ca9e62..0f2b18511 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.22.0" + versionName "0.22.1" 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 9746b6722..9ef27a1ac 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.22.0; + MARKETING_VERSION = 0.22.1; 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.22.0; + MARKETING_VERSION = 0.22.1; 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 513a3b6d7..87bfb8299 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/mobile", - "version": "0.22.0", + "version": "0.22.1", "private": true, "type": "module", "scripts": { diff --git a/apps/server/package.json b/apps/server/package.json index 35384a019..d9801b1fb 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "okcodes", - "version": "0.22.0", + "version": "0.22.1", "license": "MIT", "repository": { "type": "git", diff --git a/apps/server/src/decision/Services/DecisionProjection.ts b/apps/server/src/decision/Services/DecisionProjection.ts index 8d87c3394..02f8e5b45 100644 --- a/apps/server/src/decision/Services/DecisionProjection.ts +++ b/apps/server/src/decision/Services/DecisionProjection.ts @@ -22,10 +22,11 @@ export interface DecisionProjectionShape { readonly consultation: DecisionConsultation; readonly questions: ReadonlyArray; }) => Effect.Effect; - readonly getConsultation: (input: { - readonly consultationId: string; - }) => Effect.Effect< - { consultation: DecisionConsultation; questions: ReadonlyArray } | null, + readonly getConsultation: (input: { readonly consultationId: string }) => Effect.Effect< + { + consultation: DecisionConsultation; + questions: ReadonlyArray; + } | null, DecisionWorkspaceServiceError >; readonly listConsultationsByCaseId: (input: { diff --git a/apps/server/src/main.test.ts b/apps/server/src/main.test.ts index 0cdc05cb3..59c1af0ee 100644 --- a/apps/server/src/main.test.ts +++ b/apps/server/src/main.test.ts @@ -1,4 +1,7 @@ +import { mkdtempSync } from "node:fs"; import * as Http from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it, vi } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; @@ -17,6 +20,7 @@ import { Server, type ServerShape } from "./wsServer"; const start = vi.fn(() => undefined); const stop = vi.fn(() => undefined); let resolvedConfig: ServerConfigShape | null = null; +let testWorkspaceRoot = ""; const serverStart = Effect.acquireRelease( Effect.gen(function* () { resolvedConfig = yield* ServerConfig; @@ -29,11 +33,17 @@ const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred) // Shared service layer used by this CLI test suite. const testLayer = Layer.mergeAll( - Layer.succeed(CliConfig, { - cwd: "/tmp/t3-test-workspace", - fixPath: Effect.void, - resolveStaticDir: Effect.undefined, - } satisfies CliConfigShape), + Layer.effect( + CliConfig, + Effect.sync( + () => + ({ + cwd: testWorkspaceRoot, + fixPath: Effect.void, + resolveStaticDir: Effect.undefined, + }) satisfies CliConfigShape, + ), + ), Layer.succeed(NetService, { canListenOnHost: () => Effect.succeed(true), isPortAvailableOnLoopback: () => Effect.succeed(true), @@ -74,6 +84,7 @@ const runCli = ( beforeEach(() => { vi.clearAllMocks(); resolvedConfig = null; + testWorkspaceRoot = mkdtempSync(join(tmpdir(), "okcode-main-test-")); start.mockImplementation(() => undefined); stop.mockImplementation(() => undefined); findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); diff --git a/apps/server/src/openclawGatewayTest.ts b/apps/server/src/openclawGatewayTest.ts index f1a98059d..e04d99fa7 100644 --- a/apps/server/src/openclawGatewayTest.ts +++ b/apps/server/src/openclawGatewayTest.ts @@ -46,6 +46,10 @@ interface MutableGatewayDiagnostics { hints: string[]; } +interface RunOpenclawGatewayTestOptions { + readonly stateDir?: string | undefined; +} + interface OpenClawGatewayErrorLike { readonly message: string; readonly code?: string; @@ -75,10 +79,6 @@ function toMessage(cause: unknown, fallback: string): string { return fallback; } -function readString(value: unknown): string | undefined { - return typeof value === "string" && value.length > 0 ? value : undefined; -} - function applyGatewayError( diagnostics: MutableGatewayDiagnostics, error: OpenClawGatewayErrorLike | undefined, @@ -87,17 +87,22 @@ function applyGatewayError( return; } - diagnostics.gatewayErrorCode = error.code; + if (typeof error.code === "string") { + diagnostics.gatewayErrorCode = error.code; + } const details = error.details ?? {}; - diagnostics.gatewayErrorDetailCode = typeof details.code === "string" ? details.code : undefined; - diagnostics.gatewayErrorDetailReason = - typeof details.reason === "string" ? details.reason : undefined; - diagnostics.gatewayRecommendedNextStep = - typeof details.recommendedNextStep === "string" ? details.recommendedNextStep : undefined; - diagnostics.gatewayCanRetryWithDeviceToken = - typeof details.canRetryWithDeviceToken === "boolean" - ? details.canRetryWithDeviceToken - : undefined; + if (typeof details.code === "string") { + diagnostics.gatewayErrorDetailCode = details.code; + } + if (typeof details.reason === "string") { + diagnostics.gatewayErrorDetailReason = details.reason; + } + if (typeof details.recommendedNextStep === "string") { + diagnostics.gatewayRecommendedNextStep = details.recommendedNextStep; + } + if (typeof details.canRetryWithDeviceToken === "boolean") { + diagnostics.gatewayCanRetryWithDeviceToken = details.canRetryWithDeviceToken; + } } function pushUnique(items: string[], value: string): void { @@ -105,6 +110,17 @@ function pushUnique(items: string[], value: string): void { items.push(value); } +function formatGatewayFailureDetail( + detail: string, + diagnostics: Pick, +): string { + const code = diagnostics.gatewayErrorDetailCode; + if (!code || detail.includes(code)) { + return detail; + } + return `${detail} (${code})`; +} + function isLoopbackHost(host: string): boolean { const normalized = host.toLowerCase(); return ( @@ -245,11 +261,6 @@ async function probeHealth(parsedUrl: URL): Promise { } } -function formatSocketClose(code: number | undefined, reason: string | undefined): string | null { - if (code === undefined) return null; - return reason && reason.length > 0 ? `code ${code}: ${reason}` : `code ${code}`; -} - function buildHints( parsedUrl: URL, diagnostics: Pick< @@ -413,6 +424,7 @@ export async function runOpenclawGatewayTest( const steps: TestOpenclawGatewayStep[] = []; const diagnostics: MutableGatewayDiagnostics = createDiagnostics(); let parsedUrlForHints: URL | null = null; + let connection: Awaited> | undefined; const pushStep = ( name: string, @@ -504,11 +516,9 @@ export async function runOpenclawGatewayTest( diagnostics.hostKind = classifyGatewayHost(parsedUrl.hostname, diagnostics.resolvedAddresses); const connectStart = Date.now(); - let connection: Awaited> | undefined; try { connection = await connectOpenClawGateway({ gatewayUrl, - stateDir: options?.stateDir, sessionKey: "okcode:gateway-test", role: "operator", scopes: [...OPENCLAW_OPERATOR_SCOPES], @@ -525,7 +535,8 @@ export async function runOpenclawGatewayTest( }, userAgent: `okcode/${serverBuildInfo.version}`, locale: Intl.DateTimeFormat().resolvedOptions().locale || "en-US", - password: sharedSecret, + ...(options?.stateDir ? { stateDir: options.stateDir } : {}), + ...(sharedSecret ? { password: sharedSecret } : {}), onEvent: (event) => { pushUnique(diagnostics.observedNotifications, event.event); }, @@ -543,8 +554,27 @@ export async function runOpenclawGatewayTest( cause instanceof Error ? (cause as Error & { readonly gatewayError?: OpenClawGatewayErrorLike }).gatewayError : undefined; + const connectionStage = + cause instanceof Error + ? (cause as Error & { readonly openClawConnectionStage?: "websocket" | "handshake" }) + .openClawConnectionStage + : undefined; applyGatewayError(diagnostics, gatewayError); - const detail = toMessage(cause, "Connection failed."); + const detail = formatGatewayFailureDetail( + toMessage(cause, "Connection failed."), + diagnostics, + ); + if (connectionStage === "handshake") { + pushStep( + "WebSocket connect", + "pass", + Date.now() - connectStart, + `Connected in ${Date.now() - connectStart}ms`, + ); + applyHealthProbe(await healthPromise); + pushStep("Gateway handshake", "fail", 0, detail); + return finalize(false, detail, "Gateway handshake"); + } pushStep("WebSocket connect", "fail", Date.now() - connectStart, detail); applyHealthProbe(await healthPromise); return finalize(false, detail, "WebSocket connect"); diff --git a/apps/server/src/orchestration/Layers/ProjectionOverviewQuery.ts b/apps/server/src/orchestration/Layers/ProjectionOverviewQuery.ts index a011df914..0b6de5bb8 100644 --- a/apps/server/src/orchestration/Layers/ProjectionOverviewQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionOverviewQuery.ts @@ -31,29 +31,21 @@ import { import { ProjectionState } from "../../persistence/Services/ProjectionState.ts"; import { ProjectionProject } from "../../persistence/Services/ProjectionProjects.ts"; import { ProjectionThread } from "../../persistence/Services/ProjectionThreads.ts"; -import { ProjectionThreadProposedPlan } from "../../persistence/Services/ProjectionThreadProposedPlans.ts"; import { ProjectionThreadSession } from "../../persistence/Services/ProjectionThreadSessions.ts"; import { ORCHESTRATION_PROJECTOR_NAMES } from "./ProjectionPipeline.ts"; -const ProjectionProjectOverviewRow = ProjectionProject.mapFields({ +const ProjectionProjectOverviewRow = Schema.Struct({ + ...ProjectionProject.fields, scripts: Schema.fromJsonString(Schema.Array(ProjectScript)), -}).pipe( - Schema.extend( - Schema.Struct({ - activeThreadCount: NonNegativeInt, - }), - ), -); + activeThreadCount: NonNegativeInt, +}); -const ProjectionThreadOverviewRow = ProjectionThread.pipe( - Schema.extend( - Schema.Struct({ - lastUserMessageAt: Schema.NullOr(IsoDateTime), - pendingApprovalCount: NonNegativeInt, - pendingUserInputCount: NonNegativeInt, - }), - ), -); +const ProjectionThreadOverviewRow = Schema.Struct({ + ...ProjectionThread.fields, + lastUserMessageAt: Schema.NullOr(IsoDateTime), + pendingApprovalCount: NonNegativeInt, + pendingUserInputCount: NonNegativeInt, +}); const ProjectionLatestTurnDbRowSchema = Schema.Struct({ threadId: ProjectionThread.fields.threadId, @@ -419,7 +411,7 @@ const makeProjectionOverviewQuery = Effect.gen(function* () { projects, threads, updatedAt: - updatedAtCandidates.sort((left, right) => + updatedAtCandidates.toSorted((left, right) => left < right ? 1 : left > right ? -1 : 0, )[0] ?? new Date(0).toISOString(), }); diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts index 52e89fb15..b37d9a953 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts @@ -1103,6 +1103,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta payload: { threadId, deletedAt: now, + reason: "manual", }, }); @@ -1179,6 +1180,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta payload: { threadId: ThreadId.makeUnsafe(".."), deletedAt: now, + reason: "manual", }, }); diff --git a/apps/server/src/orchestration/Layers/ProjectionThreadDetailQuery.ts b/apps/server/src/orchestration/Layers/ProjectionThreadDetailQuery.ts index 4e55adfe9..813744e9d 100644 --- a/apps/server/src/orchestration/Layers/ProjectionThreadDetailQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionThreadDetailQuery.ts @@ -286,16 +286,29 @@ const makeProjectionThreadDetailQuery = Effect.gen(function* () { listLatestTurnRows(input), ]); - const messages: OrchestrationMessage[] = messageRows.map((row) => ({ - id: row.messageId, - role: row.role, - text: row.text, - ...(row.attachments !== null ? { attachments: row.attachments } : {}), - turnId: row.turnId, - streaming: row.isStreaming === 1, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - })); + const messages: OrchestrationMessage[] = messageRows.map((row) => { + if (row.attachments !== null) { + return { + id: row.messageId, + role: row.role, + text: row.text, + attachments: row.attachments, + turnId: row.turnId, + streaming: row.isStreaming === 1, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; + } + return { + id: row.messageId, + role: row.role, + text: row.text, + turnId: row.turnId, + streaming: row.isStreaming === 1, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; + }); const proposedPlans: OrchestrationProposedPlan[] = proposedPlanRows.map((row) => ({ id: row.planId, @@ -307,16 +320,29 @@ const makeProjectionThreadDetailQuery = Effect.gen(function* () { updatedAt: row.updatedAt, })); - const activities: OrchestrationThreadActivity[] = activityRows.map((row) => ({ - id: row.activityId, - tone: row.tone, - kind: row.kind, - summary: row.summary, - payload: row.payload, - turnId: row.turnId, - ...(row.sequence !== null ? { sequence: row.sequence } : {}), - createdAt: row.createdAt, - })); + const activities: OrchestrationThreadActivity[] = activityRows.map((row) => { + if (row.sequence !== null) { + return { + id: row.activityId, + tone: row.tone, + kind: row.kind, + summary: row.summary, + payload: row.payload, + turnId: row.turnId, + sequence: row.sequence, + createdAt: row.createdAt, + }; + } + return { + id: row.activityId, + tone: row.tone, + kind: row.kind, + summary: row.summary, + payload: row.payload, + turnId: row.turnId, + createdAt: row.createdAt, + }; + }); const checkpoints: OrchestrationCheckpointSummary[] = checkpointRows.map((row) => ({ turnId: row.turnId, diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index ec08e7020..df54fb911 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -56,6 +56,7 @@ function createThreadDeletedEvent(input: { readonly command: Pick; readonly threadId: ThreadId; readonly occurredAt: string; + readonly reason: "manual" | "limit-eviction" | "project-deleted"; }): Omit { return { ...withEventBase({ @@ -68,6 +69,7 @@ function createThreadDeletedEvent(input: { payload: { threadId: input.threadId, deletedAt: input.occurredAt, + reason: input.reason, }, }; } @@ -76,6 +78,7 @@ function createProjectDeletedEvent(input: { readonly command: Pick; readonly projectId: ProjectId; readonly occurredAt: string; + readonly reason: "manual" | "limit-eviction"; }): Omit { return { ...withEventBase({ @@ -88,6 +91,7 @@ function createProjectDeletedEvent(input: { payload: { projectId: input.projectId, deletedAt: input.occurredAt, + reason: input.reason, }, }; } @@ -145,6 +149,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, threadId: thread.id, occurredAt: command.createdAt, + reason: "limit-eviction", }), ), ...projectsToArchive.map((project) => @@ -152,6 +157,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, projectId: project.id, occurredAt: command.createdAt, + reason: "limit-eviction", }), ), ]; @@ -196,6 +202,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, projectId: command.projectId, occurredAt, + reason: "manual", }); if (threadsToArchive.length === 0) { return projectDeletedEvent; @@ -206,6 +213,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, threadId: thread.id, occurredAt, + reason: "project-deleted", }), ), projectDeletedEvent, @@ -258,6 +266,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, threadId: thread.id, occurredAt: command.createdAt, + reason: "limit-eviction", }), ); return [...archiveEvents, threadCreatedEvent]; @@ -274,6 +283,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, threadId: command.threadId, occurredAt, + reason: "manual", }); } diff --git a/apps/server/src/orchestration/projector.test.ts b/apps/server/src/orchestration/projector.test.ts index 2414ecbb9..535f75928 100644 --- a/apps/server/src/orchestration/projector.test.ts +++ b/apps/server/src/orchestration/projector.test.ts @@ -223,6 +223,7 @@ describe("orchestration projector", () => { payload: { threadId: "thread-delete", deletedAt: now, + reason: "manual", }, }), ), diff --git a/apps/server/src/persistence/Layers/DecisionConsultations.ts b/apps/server/src/persistence/Layers/DecisionConsultations.ts index e446d9449..864c86ce3 100644 --- a/apps/server/src/persistence/Layers/DecisionConsultations.ts +++ b/apps/server/src/persistence/Layers/DecisionConsultations.ts @@ -126,8 +126,8 @@ const makeDecisionConsultationRepository = Effect.gen(function* () { "DecisionConsultationRepository.listByCaseId:decodeRows", ), ), - Effect.map((rows) => - rows as ReadonlyArray>, + Effect.map( + (rows) => rows as ReadonlyArray>, ), ); diff --git a/apps/server/src/persistence/Layers/DecisionScoreSnapshots.ts b/apps/server/src/persistence/Layers/DecisionScoreSnapshots.ts index 1b39d2f40..e2d367f50 100644 --- a/apps/server/src/persistence/Layers/DecisionScoreSnapshots.ts +++ b/apps/server/src/persistence/Layers/DecisionScoreSnapshots.ts @@ -66,7 +66,9 @@ const makeDecisionScoreSnapshotRepository = Effect.gen(function* () { "DecisionScoreSnapshotRepository.listByCaseId:decodeRows", ), ), - Effect.map((rows) => rows as ReadonlyArray>), + Effect.map( + (rows) => rows as ReadonlyArray>, + ), ); return { insert, listByCaseId } satisfies DecisionScoreSnapshotRepositoryShape; diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index b50769b57..96696db2c 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -32,7 +32,9 @@ import Migration0017 from "./Migrations/017_EnvironmentVariables.ts"; import Migration0018 from "./Migrations/018_ProjectionThreadsGithubRef.ts"; import Migration0019 from "./Migrations/019_SmeKnowledgeBase.ts"; import Migration0020 from "./Migrations/020_SmeConversationProviderAuth.ts"; +import Migration0021 from "./Migrations/021_ProjectionPendingUserInputs.ts"; import Migration0022 from "./Migrations/022_DecisionWorkspace.ts"; +import Migration0023 from "./Migrations/023_ProjectionPendingUserInputsBackfill.ts"; import { Effect } from "effect"; /** @@ -66,7 +68,9 @@ const loader = Migrator.fromRecord({ "18_ProjectionThreadsGithubRef": Migration0018, "19_SmeKnowledgeBase": Migration0019, "20_SmeConversationProviderAuth": Migration0020, + "21_ProjectionPendingUserInputs": Migration0021, "22_DecisionWorkspace": Migration0022, + "23_ProjectionPendingUserInputsBackfill": Migration0023, }); /** diff --git a/apps/server/src/persistence/Migrations/023_ProjectionPendingUserInputsBackfill.ts b/apps/server/src/persistence/Migrations/023_ProjectionPendingUserInputsBackfill.ts new file mode 100644 index 000000000..9616c0471 --- /dev/null +++ b/apps/server/src/persistence/Migrations/023_ProjectionPendingUserInputsBackfill.ts @@ -0,0 +1,22 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + CREATE TABLE IF NOT EXISTS projection_pending_user_inputs ( + request_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + turn_id TEXT, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + resolved_at TEXT + ) + `; + + yield* sql` + CREATE INDEX IF NOT EXISTS idx_projection_pending_user_inputs_thread_status + ON projection_pending_user_inputs(thread_id, status) + `; +}); diff --git a/apps/server/src/persistence/Services/DecisionConsultations.ts b/apps/server/src/persistence/Services/DecisionConsultations.ts index aa8302b0b..ee1b4ceb9 100644 --- a/apps/server/src/persistence/Services/DecisionConsultations.ts +++ b/apps/server/src/persistence/Services/DecisionConsultations.ts @@ -39,7 +39,10 @@ export interface DecisionConsultationRepositoryShape { readonly upsert: (row: DecisionConsultationRow) => Effect.Effect; readonly getById: ( input: GetDecisionConsultationInput, - ) => Effect.Effect, ProjectionRepositoryError>; + ) => Effect.Effect< + import("effect").Option.Option, + ProjectionRepositoryError + >; readonly listByCaseId: ( input: ListDecisionConsultationsInput, ) => Effect.Effect, ProjectionRepositoryError>; diff --git a/apps/server/src/persistence/Services/DecisionScoreSnapshots.ts b/apps/server/src/persistence/Services/DecisionScoreSnapshots.ts index 72af98b65..e051d1106 100644 --- a/apps/server/src/persistence/Services/DecisionScoreSnapshots.ts +++ b/apps/server/src/persistence/Services/DecisionScoreSnapshots.ts @@ -1,4 +1,9 @@ -import { DecisionCaseId, IsoDateTime, NonNegativeInt, TrimmedNonEmptyString } from "@okcode/contracts"; +import { + DecisionCaseId, + IsoDateTime, + NonNegativeInt, + TrimmedNonEmptyString, +} from "@okcode/contracts"; import { Schema, ServiceMap } from "effect"; import type { Effect } from "effect"; import type { ProjectionRepositoryError } from "../Errors.ts"; diff --git a/apps/server/src/provider/Layers/OpenClawGatewayClient.ts b/apps/server/src/provider/Layers/OpenClawGatewayClient.ts index e600cb2f2..62f24711f 100644 --- a/apps/server/src/provider/Layers/OpenClawGatewayClient.ts +++ b/apps/server/src/provider/Layers/OpenClawGatewayClient.ts @@ -25,21 +25,21 @@ export interface OpenClawGatewayClientInfo { export interface OpenClawGatewayConnectOptions { readonly gatewayUrl: string; - readonly stateDir?: string; - readonly sessionKey?: string; + readonly stateDir?: string | undefined; + readonly sessionKey?: string | undefined; readonly role: "operator" | "node"; readonly scopes: ReadonlyArray; readonly client: OpenClawGatewayClientInfo; readonly userAgent: string; - readonly locale?: string; - readonly caps?: ReadonlyArray; - readonly commands?: ReadonlyArray; - readonly permissions?: Record; - readonly password?: string; - readonly deviceToken?: string; - readonly onEvent?: (event: OpenClawGatewayEvent) => void; - readonly connectTimeoutMs?: number; - readonly requestTimeoutMs?: number; + readonly locale?: string | undefined; + readonly caps?: ReadonlyArray | undefined; + readonly commands?: ReadonlyArray | undefined; + readonly permissions?: Record | undefined; + readonly password?: string | undefined; + readonly deviceToken?: string | undefined; + readonly onEvent?: ((event: OpenClawGatewayEvent) => void) | undefined; + readonly connectTimeoutMs?: number | undefined; + readonly requestTimeoutMs?: number | undefined; } export interface OpenClawGatewayEvent { @@ -50,9 +50,9 @@ export interface OpenClawGatewayEvent { } export interface OpenClawGatewayError { - readonly code?: string; + readonly code?: string | undefined; readonly message: string; - readonly details?: Record; + readonly details?: Record | undefined; } export interface OpenClawGatewayRequestResult { @@ -73,6 +73,8 @@ export interface OpenClawGatewayConnection { close(): Promise; } +type OpenClawConnectionStage = "websocket" | "handshake"; + interface PersistedOpenClawGatewayAuthState { readonly version: 1; readonly device: { @@ -98,6 +100,8 @@ interface GatewayFrame { readonly params?: unknown; readonly payload?: unknown; readonly error?: unknown; + readonly seq?: unknown; + readonly stateVersion?: unknown; } interface GatewayChallengePayload { @@ -170,7 +174,7 @@ function buildSignaturePayload(input: { readonly client: OpenClawGatewayClientInfo; readonly role: "operator" | "node"; readonly scopes: ReadonlyArray; - readonly authValue: string | undefined; + readonly authValue?: string | undefined; readonly deviceFamily: string; }): string { return JSON.stringify({ @@ -220,7 +224,7 @@ async function readAuthState(stateDir: string): Promise typeof origin === "string" && typeof token === "string", ), - ), + ) as Record, }; } catch { return null; @@ -304,14 +308,22 @@ function makeRequestError(message: string): Error { return new Error(message); } +function withConnectionStage( + error: T, + stage: OpenClawConnectionStage, +): T & { openClawConnectionStage: OpenClawConnectionStage } { + return Object.assign(error, { openClawConnectionStage: stage }); +} + function toGatewayError(frameError: unknown): OpenClawGatewayError { if (!isObject(frameError)) { return { message: "Gateway request failed." }; } const details = isObject(frameError.details) ? frameError.details : undefined; + const code = readString(frameError.code); return { message: readString(frameError.message) ?? "Gateway request failed.", - ...(readString(frameError.code) ? { code: readString(frameError.code) } : {}), + ...(code !== undefined ? { code } : {}), ...(details ? { details } : {}), }; } @@ -324,13 +336,17 @@ function buildConnectParams(input: { readonly challengeNonce: string; readonly deviceIdentity: OpenClawDeviceIdentity; readonly userAgent: string; - readonly locale?: string; - readonly caps?: ReadonlyArray; - readonly commands?: ReadonlyArray; - readonly permissions?: Record; + readonly locale?: string | undefined; + readonly caps?: ReadonlyArray | undefined; + readonly commands?: ReadonlyArray | undefined; + readonly permissions?: Record | undefined; readonly deviceFamily: string; }): Record { const signedAt = Date.now(); + const authValue = + input.auth.kind === "password" || input.auth.kind === "deviceToken" + ? input.auth.value + : undefined; return { minProtocol: OPENCLAW_PROTOCOL_VERSION, maxProtocol: OPENCLAW_PROTOCOL_VERSION, @@ -339,7 +355,7 @@ function buildConnectParams(input: { scopes: [...input.scopes], caps: [...(input.caps ?? [])], commands: [...(input.commands ?? [])], - permissions: { ...(input.permissions ?? {}) }, + permissions: { ...input.permissions }, ...(input.auth.kind === "password" ? { auth: { @@ -364,7 +380,7 @@ function buildConnectParams(input: { client: input.client, role: input.role, scopes: input.scopes, - authValue: input.authValue, + ...(authValue !== undefined ? { authValue } : {}), deviceFamily: input.deviceFamily, }), signedAt, @@ -419,6 +435,9 @@ export async function connectOpenClawGateway( for (let index = 0; index < candidateAuthSelections.length; index += 1) { const auth = candidateAuthSelections[index]; + if (auth === undefined) { + continue; + } try { const connection = await connectOnce({ gatewayUrl: options.gatewayUrl, @@ -470,15 +489,15 @@ async function connectOnce(input: { readonly auth: OpenClawGatewayAuthSelection; readonly connectTimeoutMs: number; readonly requestTimeoutMs: number; - readonly onEvent?: (event: OpenClawGatewayEvent) => void; + readonly onEvent?: ((event: OpenClawGatewayEvent) => void) | undefined; readonly client: OpenClawGatewayClientInfo; readonly role: "operator" | "node"; readonly scopes: ReadonlyArray; readonly userAgent: string; - readonly locale?: string; - readonly caps?: ReadonlyArray; - readonly commands?: ReadonlyArray; - readonly permissions?: Record; + readonly locale?: string | undefined; + readonly caps?: ReadonlyArray | undefined; + readonly commands?: ReadonlyArray | undefined; + readonly permissions?: Record | undefined; readonly deviceFamily: string; readonly sessionKey: string; }): Promise { @@ -494,6 +513,7 @@ async function connectOnce(input: { const bufferedEvents: OpenClawGatewayEvent[] = []; let connected = false; let closed = false; + let socketOpened = false; let handshakeSettled = false; let nextRequestId = 1; let challengeNonce: string | undefined; @@ -528,14 +548,15 @@ async function connectOnce(input: { } handshakeSettled = true; closed = true; - rejectChallenge?.(reason); + const stagedReason = withConnectionStage(reason, socketOpened ? "handshake" : "websocket"); + rejectChallenge?.(stagedReason); cleanup(); try { ws.close(); } catch { // ignore close errors } - reject(reason); + reject(stagedReason); }; const deliverBufferedEvents = (): void => { @@ -587,6 +608,12 @@ async function connectOnce(input: { return; } if (eventName === "connect.challenge") { + input.onEvent?.({ + event: eventName, + ...(frame.payload !== undefined ? { payload: frame.payload } : {}), + ...(typeof frame.seq === "number" ? { seq: frame.seq } : {}), + ...(typeof frame.stateVersion === "number" ? { stateVersion: frame.stateVersion } : {}), + }); const payload = isObject(frame.payload) ? (frame.payload as GatewayChallengePayload) : undefined; @@ -660,6 +687,7 @@ async function connectOnce(input: { }, input.connectTimeoutMs); ws.once("open", () => { + socketOpened = true; void (async () => { try { const challenge = await challengePromise; @@ -693,9 +721,13 @@ async function connectOnce(input: { clearTimeout(connectTimeout); handshakeSettled = true; if (!response.ok) { - const error = new Error(response.error?.message ?? "Gateway connect failed."); - (error as Error & { readonly gatewayError?: OpenClawGatewayError }).gatewayError = - response.error; + const error = withConnectionStage( + new Error(response.error?.message ?? "Gateway connect failed."), + "handshake", + ) as Error & { + gatewayError?: OpenClawGatewayError | undefined; + }; + error.gatewayError = response.error; cleanup(); try { ws.close(); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 509c9b11f..8c34aef95 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -1020,6 +1020,17 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< case ORCHESTRATION_WS_METHODS.getSnapshot: return yield* projectionReadModelQuery.getSnapshot(); + case ORCHESTRATION_WS_METHODS.getThreadDetail: { + const body = stripRequestTag(request.body); + return yield* projectionReadModelQuery + .getSnapshot() + .pipe( + Effect.map( + (snapshot) => snapshot.threads.find((thread) => thread.id === body.threadId) ?? null, + ), + ); + } + case ORCHESTRATION_WS_METHODS.dispatchCommand: { const { command } = request.body; const normalizedCommand = yield* normalizeDispatchCommand({ command }); @@ -1810,6 +1821,16 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< return yield* smeChatService.interruptMessage(body); } + case WS_METHODS.decisionListCases: + case WS_METHODS.decisionGetWorkspace: + case WS_METHODS.decisionReanalyze: + case WS_METHODS.decisionRequestConsultation: + case WS_METHODS.decisionRespondConsultation: + case WS_METHODS.decisionExecuteRecommendation: + return yield* new RouteRequestError({ + message: "Decision workspace RPCs are not wired on this server build yet.", + }); + default: { const _exhaustiveCheck: never = request.body; return yield* new RouteRequestError({ diff --git a/apps/web/package.json b/apps/web/package.json index 275116f07..68cbcbada 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/web", - "version": "0.22.0", + "version": "0.22.1", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/components/timelineHeight.ts b/apps/web/src/components/timelineHeight.ts index 58dfb512a..b536948cd 100644 --- a/apps/web/src/components/timelineHeight.ts +++ b/apps/web/src/components/timelineHeight.ts @@ -13,7 +13,7 @@ const USER_FILE_ATTACHMENT_ROW_HEIGHT_PX = 44; const USER_BUBBLE_WIDTH_RATIO = 0.8; const USER_BUBBLE_HORIZONTAL_PADDING_PX = 32; const ASSISTANT_MESSAGE_HORIZONTAL_PADDING_PX = 8; -const USER_MONO_AVG_CHAR_WIDTH_PX = 8.4; +const USER_MONO_AVG_CHAR_WIDTH_PX = 7.2; const ASSISTANT_AVG_CHAR_WIDTH_PX = 7.2; const MIN_USER_CHARS_PER_LINE = 4; const MIN_ASSISTANT_CHARS_PER_LINE = 20; diff --git a/apps/web/src/wsNativeApi.ts b/apps/web/src/wsNativeApi.ts index c7fde368a..3fa8ee279 100644 --- a/apps/web/src/wsNativeApi.ts +++ b/apps/web/src/wsNativeApi.ts @@ -342,6 +342,19 @@ export function createWsNativeApi(): NativeApi { }; }, }, + decision: { + listCases: (input) => transport.request(WS_METHODS.decisionListCases, input), + getWorkspace: (input) => transport.request(WS_METHODS.decisionGetWorkspace, input), + reanalyze: (input) => transport.request(WS_METHODS.decisionReanalyze, input), + requestConsultation: (input) => + transport.request(WS_METHODS.decisionRequestConsultation, input), + respondConsultation: (input) => + transport.request(WS_METHODS.decisionRespondConsultation, input), + executeRecommendation: (input) => + transport.request(WS_METHODS.decisionExecuteRecommendation, input), + onUpdated: (callback) => + transport.subscribe(WS_CHANNELS.decisionUpdated, (message) => callback(message.data)), + }, skills: { list: (input) => transport.request(WS_METHODS.skillList, input ?? {}), catalog: (input) => transport.request(WS_METHODS.skillCatalog, input ?? {}), @@ -388,6 +401,8 @@ export function createWsNativeApi(): NativeApi { }, orchestration: { getSnapshot: () => transport.request(ORCHESTRATION_WS_METHODS.getSnapshot), + getThreadDetail: (input) => + transport.request(ORCHESTRATION_WS_METHODS.getThreadDetail, input), dispatchCommand: (command) => transport.request(ORCHESTRATION_WS_METHODS.dispatchCommand, { command }), getTurnDiff: (input) => transport.request(ORCHESTRATION_WS_METHODS.getTurnDiff, input), diff --git a/bun.lock b/bun.lock index 71dd72551..cc2640626 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ }, "apps/desktop": { "name": "@okcode/desktop", - "version": "0.22.0", + "version": "0.22.1", "dependencies": { "effect": "catalog:", "electron": "40.6.0", @@ -103,7 +103,7 @@ }, "apps/mobile": { "name": "@okcode/mobile", - "version": "0.22.0", + "version": "0.22.1", "dependencies": { "@capacitor/android": "^8.3.0", "@capacitor/app": "^8.1.0", @@ -123,7 +123,7 @@ }, "apps/server": { "name": "okcodes", - "version": "0.22.0", + "version": "0.22.1", "bin": { "okcode": "./dist/index.mjs", }, @@ -154,7 +154,7 @@ }, "apps/web": { "name": "@okcode/web", - "version": "0.22.0", + "version": "0.22.1", "dependencies": { "@base-ui/react": "^1.2.0", "@codemirror/language": "^6.12.3", @@ -217,7 +217,7 @@ }, "packages/contracts": { "name": "@okcode/contracts", - "version": "0.22.0", + "version": "0.22.1", "dependencies": { "effect": "catalog:", }, diff --git a/docs/releases/README.md b/docs/releases/README.md index f8ed35f5f..63b40ff64 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -7,17 +7,18 @@ Use this directory for versioned release notes and asset manifests only: - `docs/releases/vX.Y.Z.md` - `docs/releases/vX.Y.Z/assets.md` -| Version | Summary | Assets | -| -------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------- | -| [0.22.0](v0.22.0.md) | Provider-aware SME auth, refreshed SME chat, settings navigation, and websocket redaction | [manifest](v0.22.0/assets.md) | -| [0.21.0](v0.21.0.md) | Terminal startup and project continuity improvements, SME auth recovery, and release alignment | [manifest](v0.21.0/assets.md) | -| [0.20.0](v0.20.0.md) | Polish the sidebar app shell, stabilize SME chat and OpenCla | [manifest](v0.20.0/assets.md) | -| [0.19.0](v0.19.0.md) | Release workflow hardening, branch-handling fixes, and release-preflight cleanup | [manifest](v0.19.0/assets.md) | -| [0.18.0](v0.18.0.md) | Workspace panel, preview pop-out controls, worktree cleanup actions, and release-flow polish | [manifest](v0.18.0/assets.md) | -| [0.17.0](v0.17.0.md) | Unified right-panel review, worktree cleanup controls, chat UX polish, and release hardening | [manifest](v0.17.0/assets.md) | -| [0.16.1](v0.16.1.md) | Release with 8 new feature(s), 3 fix(es), 19 improvement(s) | [manifest](v0.16.1/assets.md) | -| [0.16.0](v0.16.0.md) | Right-panel diff review, editable code previews, and stronger release stability | [manifest](v0.16.0/assets.md) | -| [0.15.0](v0.15.0.md) | Brand refresh, scoped preview tabs, redesigned chat home, and rebase-aware branch sync | [manifest](v0.15.0/assets.md) | -| [0.14.0](v0.14.0.md) | Inline diffs, prompt enhancement, auto-refreshing file tree, and link-based mobile pairing | [manifest](v0.14.0/assets.md) | +| Version | Summary | Assets | +| -------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------- | +| [0.22.1](v0.22.1.md) | Sidebar density controls, notification diagnostics, decision workspace groundwork, and OpenClaw hardening | [manifest](v0.22.1/assets.md) | +| [0.22.0](v0.22.0.md) | Provider-aware SME auth, refreshed SME chat, settings navigation, and websocket redaction | [manifest](v0.22.0/assets.md) | +| [0.21.0](v0.21.0.md) | Terminal startup and project continuity improvements, SME auth recovery, and release alignment | [manifest](v0.21.0/assets.md) | +| [0.20.0](v0.20.0.md) | Polish the sidebar app shell, stabilize SME chat and OpenCla | [manifest](v0.20.0/assets.md) | +| [0.19.0](v0.19.0.md) | Release workflow hardening, branch-handling fixes, and release-preflight cleanup | [manifest](v0.19.0/assets.md) | +| [0.18.0](v0.18.0.md) | Workspace panel, preview pop-out controls, worktree cleanup actions, and release-flow polish | [manifest](v0.18.0/assets.md) | +| [0.17.0](v0.17.0.md) | Unified right-panel review, worktree cleanup controls, chat UX polish, and release hardening | [manifest](v0.17.0/assets.md) | +| [0.16.1](v0.16.1.md) | Release with 8 new feature(s), 3 fix(es), 19 improvement(s) | [manifest](v0.16.1/assets.md) | +| [0.16.0](v0.16.0.md) | Right-panel diff review, editable code previews, and stronger release stability | [manifest](v0.16.0/assets.md) | +| [0.15.0](v0.15.0.md) | Brand refresh, scoped preview tabs, redesigned chat home, and rebase-aware branch sync | [manifest](v0.15.0/assets.md) | +| [0.14.0](v0.14.0.md) | Inline diffs, prompt enhancement, auto-refreshing file tree, and link-based mobile pairing | [manifest](v0.14.0/assets.md) | When release policy changes, update [`docs/release.md`](../release.md) instead of duplicating the workflow matrix here. diff --git a/docs/releases/v0.22.1.md b/docs/releases/v0.22.1.md new file mode 100644 index 000000000..e9e0fc72e --- /dev/null +++ b/docs/releases/v0.22.1.md @@ -0,0 +1,70 @@ +# OK Code v0.22.1 + +**Date:** 2026-04-10 +**Tag:** [`v0.22.1`](https://github.com/OpenKnots/okcode/releases/tag/v0.22.1) + +## Summary + +Ship the `0.22.x` patch follow-up with sidebar density controls, richer diagnostics, decision workspace groundwork, stronger OpenClaw plumbing, and desktop and SME workflow polish. + +## Highlights + +- **Add sidebar density settings and refresh the default typography and theme tokens across the web app.** +- **Surface expandable notification diagnostics, copy support, and connection test controls so failures are easier to inspect before retrying.** +- **Lay the groundwork for decision workspaces with new contracts, persistence, projections, and WebSocket wiring.** +- **Strengthen provider integrations by extracting the OpenClaw gateway client, adding auth fallback handling, and switching SME Claude flows to Claude Code CLI.** +- **Polish desktop and chat flows with route-preserving pop-out windows, wider preview viewport inputs, markdown SME replies, and safer diff handling.** + +## Breaking changes + +- None. + +## Upgrade and install + +- **CLI:** `npm install -g okcodes@0.22.1` after the package is published to npm. +- **Desktop:** Download from [GitHub Releases](https://github.com/OpenKnots/okcode/releases/tag/v0.22.1). Filenames are listed in [assets.md](v0.22.1/assets.md). +- **iOS:** Available via TestFlight when the coordinated release workflow completes successfully. + +## Detailed changes + +### UI density, typography, and navigation polish + +- Added sidebar density settings so operators can tighten or relax the navigation layout without changing the existing flow. +- Switched the default web typography to Oxanium and Space Grotesk and refreshed theme tokens to keep the updated chat shell visually consistent. +- Widened preview viewport inputs, preserved the active thread route in desktop pop-out windows, and opened GitHub links from the preview popout in the external browser. +- Replaced the draft upload icon with a close action and kept SME replies rendering as markdown for clearer thread reading. + +### Diagnostics and action handling + +- Added expandable notification details with copy-ready diagnostics so runtime and connection failures can be inspected and shared without digging through logs. +- Added connection test controls in the UI to make provider and transport troubleshooting faster during setup and recovery. +- Added stop support for pending git actions to keep long-running operations interruptible from the existing flow. +- Ignored expected redacted auth shutdown noise in Codex logs and deferred an empty diff guard until after hook setup to avoid false-positive failures. + +### Decision workspace and companion groundwork + +- Added new decision workspace contracts plus server-side persistence layers, migrations, projection services, and WebSocket wiring needed for future consultation workflows. +- Added pending user input projections and thread overview and detail queries so orchestration state can expose richer action context. +- Added companion pairing contracts and mobile pairing stubs, with supporting test updates, to prepare the mobile companion flow for broader session coordination. + +### Provider and integration updates + +- Extracted the OpenClaw gateway client into a dedicated layer with auth fallback support and updated the handshake test to the modern connect flow. +- Switched SME Claude integrations to Claude Code CLI while keeping the provider-auth flow aligned with the current SME experience. +- Normalized React language ids for syntax highlighting so editor and diff rendering stay predictable across React-related file associations. + +### Maintainer tooling + +- Added OpenClaw maintainer workflow skills to support faster GHSA, review, prep, merge, and PR operations. +- Regenerated VS Code icon manifests so shipped file associations and icon metadata stay aligned with the repo sources. +- Added planning docs for sidebar and LM Studio follow-up work without changing the current release behavior. + +## Release verification references + +- Review the [asset manifest](v0.22.1/assets.md) to confirm every expected GitHub Release attachment is present. +- Use the [rollout checklist](v0.22.1/rollout-checklist.md) to walk the coordinated release from preflight through post-release verification. +- Use the [soak test plan](v0.22.1/soak-test-plan.md) to validate the highest-risk surfaces after the tag is live. + +## 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. diff --git a/docs/releases/v0.22.1/assets.md b/docs/releases/v0.22.1/assets.md new file mode 100644 index 000000000..78c318fd6 --- /dev/null +++ b/docs/releases/v0.22.1/assets.md @@ -0,0 +1,67 @@ +# v0.22.1 — Release assets (manifest) + +Binaries are **not** stored in this git repository; they are attached to the [GitHub Release for `v0.22.1`](https://github.com/OpenKnots/okcode/releases/tag/v0.22.1) by the [coordinated release workflow](../../.github/workflows/release.yml). + +The GitHub Release also includes **documentation attachments** with stable filenames: + +| File | Source in repo | +| --------------------------- | ------------------------------------- | +| `okcode-CHANGELOG.md` | [CHANGELOG.md](../../../CHANGELOG.md) | +| `okcode-RELEASE-NOTES.md` | [v0.22.1.md](../v0.22.1.md) | +| `okcode-ASSETS-MANIFEST.md` | This file | + +After the workflow completes, the release should contain the coordinated desktop outputs below. Filenames may include the product name `OK Code` and the version string `0.22.1`. + +## Desktop installers and payloads + +| Platform | Kind | Expected attachment pattern | +| ------------------- | -------------- | --------------------------- | +| macOS Apple Silicon | DMG (signed) | `*.dmg` (arm64) | +| macOS | ZIP (updater) | `*.zip` | +| Linux x64 | AppImage | `*.AppImage` | +| Windows x64 | NSIS installer | `*.exe` | + +The release workflow also uploads updater manifests and differential payload metadata: + +- `latest-mac*.yml` +- `latest-linux.yml` +- `latest.yml` +- `*.blockmap` + +### Intel compatibility artifact + +The separate [`release-intel-compat.yml`](../../.github/workflows/release-intel-compat.yml) workflow produces the non-blocking macOS x64 compatibility build. It is **not** part of the coordinated stable release attachment set unless it is uploaded separately after that workflow runs. + +### macOS code signing and notarization + +All coordinated macOS DMG and ZIP payloads are expected to be code-signed with the Apple Developer ID certificate and notarized before release publication. Gatekeeper verifies the signature on first launch. + +## Electron updater metadata + +| File | Purpose | +| ------------------ | --------------------------------------------------------- | +| `latest-mac*.yml` | macOS update manifest | +| `latest-linux.yml` | Linux update manifest | +| `latest.yml` | Windows update manifest | +| `*.blockmap` | Differential download block maps for Electron auto-update | + +## iOS (TestFlight) + +The iOS build is uploaded directly to App Store Connect / TestFlight by the coordinated release workflow. No IPA is attached to the GitHub Release. + +| Detail | Value | +| ----------------- | ----------------------------- | +| Bundle ID | `com.openknots.okcode.mobile` | +| Marketing version | `0.22.1` | +| Build number | Set from `GITHUB_RUN_NUMBER` | + +## Rollout documentation + +| Document | 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 | + +## Checksums + +SHA-256 checksums are not committed in-repo. Verify downloads through the GitHub release UI or with `gh release download` followed by local checksum generation if needed. diff --git a/docs/releases/v0.22.1/rollout-checklist.md b/docs/releases/v0.22.1/rollout-checklist.md new file mode 100644 index 000000000..a50eb850c --- /dev/null +++ b/docs/releases/v0.22.1/rollout-checklist.md @@ -0,0 +1,65 @@ +# v0.22.1 Rollout Checklist + +Step-by-step playbook for the v0.22.1 release. Each phase must complete before advancing. + +## Phase 0: Pre-flight + +- [ ] Verify all release package versions are `0.22.1`: + - `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.22.1`. +- [ ] Confirm `CHANGELOG.md` has `## [0.22.1] - 2026-04-10`. +- [ ] Confirm `docs/releases/v0.22.1.md` exists with Summary, Highlights, Upgrade, and Detailed changes sections. +- [ ] Confirm `docs/releases/v0.22.1/assets.md` exists and lists every expected attachment class. +- [ ] Confirm `docs/releases/v0.22.1/rollout-checklist.md` and `docs/releases/v0.22.1/soak-test-plan.md` exist. +- [ ] Confirm `docs/releases/README.md` includes the v0.22.1 row. +- [ ] Run `bun run release:validate 0.22.1`. +- [ ] 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.22.1`. +- [ ] Verify the coordinated `release.yml` workflow starts. +- [ ] Monitor the pipeline through Preflight, Desktop builds, iOS signing preflight, optional iOS TestFlight, Publish GitHub Release, Finalize release, and optional CLI publish if started through manual dispatch. + +### Asset verification + +- [ ] GitHub Release body matches `docs/releases/v0.22.1.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 + +- [ ] `npx --yes okcodes@0.22.1 --version` returns `0.22.1`. +- [ ] 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 sidebar density controls, notification diagnostics, decision workspace projections, OpenClaw routing, and desktop pop-out flows. diff --git a/docs/releases/v0.22.1/soak-test-plan.md b/docs/releases/v0.22.1/soak-test-plan.md new file mode 100644 index 000000000..188bc3268 --- /dev/null +++ b/docs/releases/v0.22.1/soak-test-plan.md @@ -0,0 +1,48 @@ +# v0.22.1 Soak Test Plan + +Structured validation plan for the highest-risk surfaces in v0.22.1. + +## 1. Sidebar density and visual refresh + +| Step | Expected | Pass | +| ------------------------------------------------- | --------------------------------------------------------------------- | ---- | +| Open the main chat sidebar with the default theme | Sidebar spacing, typography, and section structure render cleanly | [ ] | +| Switch between available sidebar density settings | Density updates immediately without breaking navigation or truncation | [ ] | +| Navigate projects and threads with each density | Labels, badges, and controls remain readable and correctly aligned | [ ] | +| Reload the app after changing density | The chosen density persists and restores without layout regressions | [ ] | + +## 2. Diagnostics and pending actions + +| Step | Expected | Pass | +| ----------------------------------------------------- | --------------------------------------------------------------------------- | ---- | +| Trigger a provider or connection error | Notification expands to show diagnostic details without leaking credentials | [ ] | +| Use the diagnostics copy action | Copied content includes actionable details and preserves redaction | [ ] | +| Use connection test controls on a configured provider | The control reports success or failure without blocking the rest of the UI | [ ] | +| Start a git action and stop it while pending | The action stops cleanly and the thread state remains consistent | [ ] | + +## 3. Decision workspace and user-input projections + +| Step | Expected | Pass | +| ------------------------------------------------------- | --------------------------------------------------------------------- | ---- | +| Open a thread with orchestration activity | Thread overview and detail queries return the expected projected data | [ ] | +| Inspect pending user input state for an active thread | Pending input projections appear in the thread model without drift | [ ] | +| Exercise reconnect or session resume on projected state | Projection-backed thread data survives reload and reconnect correctly | [ ] | +| Inspect new decision-related persistence after activity | Decision tables and snapshots populate without migration errors | [ ] | + +## 4. Provider and OpenClaw routing + +| Step | Expected | Pass | +| ----------------------------------------------------------- | ---------------------------------------------------------------------- | ---- | +| Run an SME Claude conversation after upgrading | The flow routes through Claude Code CLI and completes normally | [ ] | +| Exercise OpenClaw gateway auth with a valid configuration | Auth fallback behavior succeeds without manual recovery | [ ] | +| Trigger an OpenClaw connection failure | Diagnostics surface cleanly and expected shutdown noise stays filtered | [ ] | +| Review syntax highlighting on React-related files and diffs | React language ids render with the intended highlighting configuration | [ ] | + +## 5. Desktop pop-out and preview polish + +| Step | Expected | Pass | +| ------------------------------------------------------ | ----------------------------------------------------------------- | ---- | +| Pop a thread out into a desktop window | The active thread route is preserved in the new window | [ ] | +| Open a GitHub link from the preview popout | The link opens externally instead of hijacking the in-app preview | [ ] | +| Adjust wider preview viewport inputs | Values remain usable and layout controls do not clip | [ ] | +| Run desktop smoke and release smoke on the release tag | Packaging and release-only workflow steps remain green | [ ] | diff --git a/packages/contracts/package.json b/packages/contracts/package.json index e9256924d..9d6bbfcf3 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@okcode/contracts", - "version": "0.22.0", + "version": "0.22.1", "private": true, "files": [ "dist" diff --git a/packages/contracts/src/decision.ts b/packages/contracts/src/decision.ts index 7d34dc88c..f98c161bd 100644 --- a/packages/contracts/src/decision.ts +++ b/packages/contracts/src/decision.ts @@ -110,7 +110,7 @@ export type DecisionPrincipleResult = typeof DecisionPrincipleResult.Type; export const DecisionConfidenceFactor = Schema.Struct({ id: DecisionFactorId, label: TrimmedNonEmptyString, - score: NonNegativeInt.pipe(Schema.clamp(0, 100)), + score: NonNegativeInt.check(Schema.isBetween({ minimum: 0, maximum: 100 })), weight: Schema.Number, weightedPoints: Schema.Number, why: TrimmedNonEmptyString, @@ -129,7 +129,7 @@ export const DecisionNextContextHint = Schema.Struct({ export type DecisionNextContextHint = typeof DecisionNextContextHint.Type; export const DecisionConfidenceAnalysis = Schema.Struct({ - score: NonNegativeInt.pipe(Schema.clamp(0, 100)), + score: NonNegativeInt.check(Schema.isBetween({ minimum: 0, maximum: 100 })), riskTier: DecisionRiskTier, autoExecuteEligible: Schema.Boolean, scoreDelta: Schema.Int, @@ -193,7 +193,7 @@ export const DecisionCaseSummary = Schema.Struct({ title: TrimmedNonEmptyString, subtitle: Schema.String, conflictKind: DecisionConflictKind, - score: NonNegativeInt.pipe(Schema.clamp(0, 100)), + score: NonNegativeInt.check(Schema.isBetween({ minimum: 0, maximum: 100 })), riskTier: DecisionRiskTier, consultationStatus: DecisionConsultationStatus, updatedAt: IsoDateTime, diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 710d79940..049eedf87 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -120,7 +120,7 @@ import type { OrchestrationGetTurnDiffInput, OrchestrationGetTurnDiffResult, OrchestrationEvent, - OrchestrationOverviewSnapshot, + OrchestrationReadModel, OrchestrationThread, } from "./orchestration"; import type { @@ -479,7 +479,7 @@ export interface NativeApi { testOpenclawGateway: (input: TestOpenclawGatewayInput) => Promise; }; orchestration: { - getSnapshot: () => Promise; + getSnapshot: () => Promise; getThreadDetail: ( input: OrchestrationGetThreadDetailInput, ) => Promise; diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index e65d94448..c765ffbb4 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -746,7 +746,7 @@ export type ProjectDeletedReason = typeof ProjectDeletedReason.Type; export const ProjectDeletedPayload = Schema.Struct({ projectId: ProjectId, deletedAt: IsoDateTime, - reason: ProjectDeletedReason, + reason: ProjectDeletedReason.pipe(Schema.withDecodingDefault(() => "manual")), }); export const ThreadCreatedPayload = Schema.Struct({ @@ -771,7 +771,7 @@ export type ThreadDeletedReason = typeof ThreadDeletedReason.Type; export const ThreadDeletedPayload = Schema.Struct({ threadId: ThreadId, deletedAt: IsoDateTime, - reason: ThreadDeletedReason, + reason: ThreadDeletedReason.pipe(Schema.withDecodingDefault(() => "manual")), }); export const ThreadMetaUpdatedPayload = Schema.Struct({ @@ -1085,7 +1085,7 @@ export type DispatchResult = typeof DispatchResult.Type; export const OrchestrationGetSnapshotInput = Schema.Struct({}); export type OrchestrationGetSnapshotInput = typeof OrchestrationGetSnapshotInput.Type; -const OrchestrationGetSnapshotResult = OrchestrationOverviewSnapshot; +const OrchestrationGetSnapshotResult = OrchestrationReadModel; export type OrchestrationGetSnapshotResult = typeof OrchestrationGetSnapshotResult.Type; export const OrchestrationGetThreadDetailInput = Schema.Struct({ diff --git a/packages/contracts/src/ws.ts b/packages/contracts/src/ws.ts index 8d35bd821..21bf9c6d7 100644 --- a/packages/contracts/src/ws.ts +++ b/packages/contracts/src/ws.ts @@ -6,6 +6,7 @@ import { OrchestrationEvent, ORCHESTRATION_WS_CHANNELS, OrchestrationGetFullThreadDiffInput, + OrchestrationGetThreadDetailInput, ORCHESTRATION_WS_METHODS, OrchestrationGetSnapshotInput, OrchestrationGetTurnDiffInput, @@ -340,6 +341,9 @@ const WebSocketRequestBody = Schema.Union([ tagRequestBody(WS_METHODS.decisionRespondConsultation, DecisionRespondConsultationInput), tagRequestBody(WS_METHODS.decisionExecuteRecommendation, DecisionExecuteRecommendationInput), + // Orchestration detail methods + tagRequestBody(ORCHESTRATION_WS_METHODS.getThreadDetail, OrchestrationGetThreadDetailInput), + // Terminal methods tagRequestBody(WS_METHODS.terminalOpen, TerminalOpenInput), tagRequestBody(WS_METHODS.terminalWrite, TerminalWriteInput), From 3b131e8babd550e9dc4622ae2fb1f50dbf8a7586 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 10 Apr 2026 14:39:03 -0500 Subject: [PATCH 2/2] Document release readiness and compatibility fixes - Add 0.22.1 notes for orchestration and thread-detail compatibility - Record projection backfill, gateway diagnostics, test isolation, and layout fixes --- CHANGELOG.md | 5 +++++ docs/releases/v0.22.1.md | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af58a8393..0d5b42b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ See [docs/releases/v0.22.1.md](docs/releases/v0.22.1.md) for full notes and [doc - Ignore expected redacted auth shutdown noise in Codex logs. - Normalize React language ids for syntax highlighting. - Defer the empty diff guard until after hook setup. +- Restore orchestration snapshot and thread-detail compatibility across shared contracts and WebSocket wiring. +- Backfill the pending user input projection table for already-upgraded state directories. +- Improve OpenClaw gateway handshake diagnostics and connection-stage reporting. +- Isolate CLI test state directories to avoid SQLite lock contention during release validation. +- Tune long user-message timeline height estimation so browser layout stays aligned with validated rendering. ## [0.22.0] - 2026-04-09 diff --git a/docs/releases/v0.22.1.md b/docs/releases/v0.22.1.md index e9e0fc72e..3b31c185f 100644 --- a/docs/releases/v0.22.1.md +++ b/docs/releases/v0.22.1.md @@ -53,6 +53,14 @@ Ship the `0.22.x` patch follow-up with sidebar density controls, richer diagnost - Switched SME Claude integrations to Claude Code CLI while keeping the provider-auth flow aligned with the current SME experience. - Normalized React language ids for syntax highlighting so editor and diff rendering stay predictable across React-related file associations. +### Release readiness and compatibility fixes + +- Restored orchestration snapshot and thread detail compatibility across shared contracts and WebSocket methods so the current server and web builds agree on the runtime payload shape. +- Added a pending user input projection backfill migration so already-upgraded state directories do not fail startup or tests with a missing projection table. +- Improved OpenClaw gateway handshake diagnostics to report websocket-stage versus handshake-stage failures clearly, including pairing-required responses. +- Isolated CLI test state directories to avoid SQLite lock contention during the full local release validation suite. +- Tuned long user-message timeline height estimation so the browser chat layout stays aligned with the validated rendering path. + ### Maintainer tooling - Added OpenClaw maintainer workflow skills to support faster GHSA, review, prep, merge, and PR operations.