You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/06 11:15:13 UTC
(maka) branch main updated: fix(desktop): recover outdated managed host storage at startup (#4918)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new df030ab28b fix(desktop): recover outdated managed host storage at startup (#4918)
df030ab28b is described below
commit df030ab28b8532c63121130c28032430be326984
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Sep 6 19:15:08 2026 +0800
fix(desktop): recover outdated managed host storage at startup (#4918)
Validate local storage before admitting the Host candidate and route required migrations through managed startup recovery without granting Desktop migration authority.
Generated-by: Codex
---
.../main/__tests__/runtime-host-client-uds.test.ts | 4 ++
.../runtime-host-desktop-candidate.test.ts | 80 ++++++++++++++++++++++
.../runtime-host-startup-recovery.test.ts | 14 ++--
.../src/main/runtime-host-desktop-candidate.ts | 5 ++
.../src/main/runtime-host-startup-recovery.ts | 4 ++
.../src/__tests__/operational-state-store.test.ts | 8 +++
.../storage/src/operational-state-store-public.ts | 1 +
packages/storage/src/operational-state-store.ts | 11 ++-
8 files changed, 121 insertions(+), 6 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
index da520348ce..f287676e77 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
@@ -27,6 +27,7 @@ import type { BotRegistry } from '@maka/runtime/bots';
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
import type { MakaTool } from '@maka/runtime/tool-runtime';
import { connectRuntimeHost } from '@maka/runtime-host/client';
+import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import {
RUNTIME_HOST_PROTOCOL_VERSION,
type SessionCatalogProjection,
@@ -250,6 +251,9 @@ test('drives the renderer Session catalog facade through real UDS framing', asyn
});
const ipc = ipcHarness();
const changes: Array<{ reason: string; sessionId?: string }> = [];
+ // This fixture replaces the real execution composition; initialize its
+ // owned storage before Desktop admits the local candidate.
+ acquireOperationalStateDatabase(base).close();
const started = await startDesktopRuntimeHostCandidate({
rootPath: base,
candidateEntrypoint: new URL('file:///unused-runtime-host-candidate.js'),
diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
index 6a71834883..03c14672be 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
@@ -19,7 +19,12 @@
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { DatabaseSync } from 'node:sqlite';
import test from 'node:test';
+import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import type { IpcMain } from 'electron';
import type { BotIncomingMessage, BotRegistry } from '@maka/runtime/bots';
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
@@ -55,6 +60,7 @@ import { RuntimeHostSessionObservationRegistry } from '../runtime-host-session-o
import { RuntimeHostReconnectingIpcMain } from '../runtime-host-reconnecting-ipc-main.js';
import { desktopSessionResourceKey } from '../../shared/runtime-host-identity.js';
import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
+import { startDesktopRuntimeHostWithRecovery } from '../runtime-host-startup-recovery.js';
const TEST_HOST_ID = 'a'.repeat(64);
const TEST_TARGET_EPOCH = 'test-target-epoch';
@@ -84,6 +90,80 @@ test('uses the manager-owned launch barrier for local candidate startup', async
assert.equal(connectedRoot, 'C:\\workspace');
});
+test('updates a protocol-compatible managed Host before exposing a candidate over its old storage', async (t) => {
+ const root = await mkdtemp(join(tmpdir(), 'maka-desktop-managed-schema-'));
+ t.after(() => rm(root, { recursive: true, force: true }));
+ acquireOperationalStateDatabase(root).close();
+ const databasePath = join(root, 'runtime.sqlite');
+ const legacy = new DatabaseSync(databasePath);
+ legacy.exec(`
+ DROP TABLE usage_model_call_attempts;
+ CREATE TABLE usage_model_call_attempts (
+ attempt_id TEXT PRIMARY KEY,
+ completed_at INTEGER NOT NULL,
+ record_json TEXT NOT NULL,
+ session_id TEXT
+ );
+ INSERT INTO usage_model_call_attempts VALUES ('retained', 1, '{}', 'deleted-session');
+ UPDATE operational_schema_migrations SET version = 6 WHERE scope = 'usage';
+ `);
+ legacy.close();
+ const ipc = ipcHarness();
+ const old = connectionHarness('old');
+ const updated = connectionHarness('updated');
+ let starts = 0;
+ let repairs = 0;
+ const candidate = await startDesktopRuntimeHostWithRecovery({
+ start: async () => {
+ const host = starts++ === 0 ? old : updated;
+ const result = await startDesktopRuntimeHostCandidate({
+ ...deps(ipc),
+ workspaceRoot: root,
+ rootPath: root,
+ candidateEntrypoint: 'unused.js',
+ candidateLaunchBarrier: {
+ connect: async () => ({
+ kind: 'connected',
+ connection: host.connection,
+ registration: { lifecycleMode: 'supervised', pid: 123 },
+ }),
+ },
+ } as unknown as DesktopRuntimeHostCandidateStartInput);
+ assert.equal(result.kind, 'ready');
+ if (result.kind !== 'ready') throw new Error('Expected a ready candidate');
+ return result.candidate;
+ },
+ repair: async (authority) => {
+ repairs += 1;
+ assert.deepEqual(authority, { allowManualUpdate: false, allowInterruptActiveTasks: false });
+ assert.equal(old.closeCalls, 1, 'release the old connection before managed update');
+ assert.equal(old.capabilityRegistrations, 0, 'do not expose capabilities before storage admission');
+ assert.equal(ipc.size, 0);
+ const preserved = new DatabaseSync(databasePath, { readOnly: true });
+ try {
+ assert.equal(preserved.prepare("SELECT version FROM operational_schema_migrations WHERE scope = 'usage'").get()?.version, 6);
+ assert.equal(preserved.prepare("SELECT record_json FROM usage_model_call_attempts WHERE attempt_id = 'retained'").get()?.record_json, '{}');
+ } finally {
+ preserved.close();
+ }
+ // Simulate the updated owning Host, not Desktop, performing migration.
+ acquireOperationalStateDatabase(root).close();
+ return { kind: 'repaired' };
+ },
+ prompt: async () => { throw new Error('No prompt needed for an idle automatically updatable Host'); },
+ });
+ t.after(() => candidate.close());
+ assert.equal(starts, 2);
+ assert.equal(repairs, 1);
+ assert.equal(updated.capabilityRegistrations, 1);
+ const current = acquireOperationalStateDatabase(root, { schemaMigration: 'require_current' });
+ try {
+ assert.equal(current.database.prepare("SELECT session_id FROM usage_model_call_attempts WHERE attempt_id = 'retained'").get()?.session_id, 'deleted-session');
+ } finally {
+ current.close();
+ }
+});
+
test('formats bounded local Host exit evidence without leaking stderr secrets', () => {
const diagnostic = formatLocalRuntimeHostProcessExitDiagnostic(42, {
code: 23,
diff --git a/apps/desktop/src/main/__tests__/runtime-host-startup-recovery.test.ts b/apps/desktop/src/main/__tests__/runtime-host-startup-recovery.test.ts
index 8074ee6915..8b8cc43a91 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-startup-recovery.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-startup-recovery.test.ts
@@ -20,6 +20,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { runtimeHostStartupError } from "@maka/runtime-host/client";
+import { OperationalStateMigrationBlockedError } from '@maka/storage/operational-state-store';
import { startDesktopRuntimeHostWithRecovery } from "../runtime-host-startup-recovery.js";
test("repairs a managed Host once and resumes startup without asking the user", async () => {
@@ -55,7 +56,10 @@ test("repairs a managed Host once and resumes startup without asking the user",
assert.equal(prompts, 0);
});
-test("separates manual update consent from active-work interruption", async () => {
+for (const failure of [
+ runtimeHostStartupError('managed_root_requires_operator'),
+ new OperationalStateMigrationBlockedError(new Error('Host migration required'), 'requires_host_migration'),
+]) test(`separates manual update consent from active-work interruption: ${failure.name}`, async () => {
let starts = 0;
const repairModes: Array<{
readonly allowManualUpdate: boolean;
@@ -67,7 +71,7 @@ test("separates manual update consent from active-work interruption", async () =
start: async () => {
starts += 1;
if (starts === 1)
- throw runtimeHostStartupError("managed_root_requires_operator");
+ throw failure;
return "ready";
},
repair: async (authority) => {
@@ -92,8 +96,10 @@ test("separates manual update consent from active-work interruption", async () =
assert.deepEqual(prompts, [false, true]);
});
-test("does not offer managed repair for an unrelated startup failure", async () => {
- const failure = new Error("renderer prerequisites failed");
+for (const failure of [
+ new Error('renderer prerequisites failed'),
+ new OperationalStateMigrationBlockedError(new Error('unsupported newer schema')),
+]) test(`does not offer managed repair for an unrelated startup failure: ${failure.name}`, async () => {
let repairs = 0;
let prompts = 0;
diff --git a/apps/desktop/src/main/runtime-host-desktop-candidate.ts b/apps/desktop/src/main/runtime-host-desktop-candidate.ts
index e323bf6c4a..9d41e42e0e 100644
--- a/apps/desktop/src/main/runtime-host-desktop-candidate.ts
+++ b/apps/desktop/src/main/runtime-host-desktop-candidate.ts
@@ -18,6 +18,7 @@
*/
import { randomUUID } from "node:crypto";
+import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import type { IpcMain } from "electron";
import type { ActiveInteractionRequestEvent } from '@maka/core/events';
import { redactSecrets } from '@maka/core/redaction';
@@ -344,6 +345,10 @@ export async function startDesktopRuntimeHostCandidate(
if (connection.kind !== "connected") return connection;
observeLocalRuntimeHostProcess(connection.spawnedProcess);
try {
+ // A resident managed Host can speak this protocol while still using an
+ // older storage schema. Validate the shared local database before exposing
+ // any candidate services, so startup recovery can update its owning Host.
+ acquireOperationalStateDatabase(input.rootPath, { schemaMigration: 'require_current' }).close();
return {
kind: "ready",
candidate: await createDesktopRuntimeHostCandidate(
diff --git a/apps/desktop/src/main/runtime-host-startup-recovery.ts b/apps/desktop/src/main/runtime-host-startup-recovery.ts
index 3f6b282986..5d68697609 100644
--- a/apps/desktop/src/main/runtime-host-startup-recovery.ts
+++ b/apps/desktop/src/main/runtime-host-startup-recovery.ts
@@ -18,6 +18,7 @@
*/
import { RuntimeHostStartupError } from "@maka/runtime-host/client";
+import { OperationalStateMigrationBlockedError } from '@maka/storage/operational-state-store';
export type DesktopRuntimeHostStartupRepairResult =
| { readonly kind: "repaired" }
@@ -121,6 +122,9 @@ export async function startDesktopRuntimeHostWithRecovery<T>(input: {
}
export function canRepairManagedRuntimeHostStartup(error: Error): boolean {
+ if (error instanceof OperationalStateMigrationBlockedError) {
+ return error.reason === 'requires_host_migration';
+ }
return (
error instanceof RuntimeHostStartupError &&
(error.reason === "managed_root_requires_operator" ||
diff --git a/packages/storage/src/__tests__/operational-state-store.test.ts b/packages/storage/src/__tests__/operational-state-store.test.ts
index f6274475f6..1264656455 100644
--- a/packages/storage/src/__tests__/operational-state-store.test.ts
+++ b/packages/storage/src/__tests__/operational-state-store.test.ts
@@ -126,6 +126,7 @@ test('a non-owner rejects an older schema without migrating it behind the Runtim
}),
(error: unknown) =>
error instanceof OperationalStateMigrationBlockedError &&
+ error.reason === 'requires_host_migration' &&
/requires migration by its Runtime Host/u.test(error.message),
);
@@ -1295,6 +1296,13 @@ test('rejects a newer scope before migrating an older scope', async () => {
() => acquireOperationalStateDatabase(root),
/Operational schema usage is newer than supported/,
);
+ assert.throws(
+ () => acquireOperationalStateDatabase(root, { schemaMigration: 'require_current' }),
+ (error: unknown) =>
+ error instanceof OperationalStateMigrationBlockedError &&
+ error.reason === 'blocked' &&
+ /Operational schema usage is newer than supported/u.test(error.message),
+ );
const preserved = new DatabaseSync(databasePath, { readOnly: true });
try {
diff --git a/packages/storage/src/operational-state-store-public.ts b/packages/storage/src/operational-state-store-public.ts
index 1fd6c8b6d9..bacb95b2c3 100644
--- a/packages/storage/src/operational-state-store-public.ts
+++ b/packages/storage/src/operational-state-store-public.ts
@@ -26,6 +26,7 @@
*/
export {
acquireOperationalStateDatabase,
+ OperationalStateMigrationBlockedError,
OPERATIONAL_STATE_DATABASE_NAME,
OPERATIONAL_STATE_SCHEMA_VERSION,
resolveOperationalStateDatabasePath,
diff --git a/packages/storage/src/operational-state-store.ts b/packages/storage/src/operational-state-store.ts
index 63faeba31c..77def1c7b0 100644
--- a/packages/storage/src/operational-state-store.ts
+++ b/packages/storage/src/operational-state-store.ts
@@ -153,7 +153,10 @@ export interface OperationalStateDatabaseOptions {
export class OperationalStateMigrationBlockedError extends Error {
readonly code = 'operational_state_migration_blocked';
- constructor(cause: unknown) {
+ constructor(
+ cause: unknown,
+ readonly reason: 'requires_host_migration' | 'blocked' = 'blocked',
+ ) {
super(cause instanceof Error ? cause.message : 'Operational state migration is blocked', {
cause,
});
@@ -202,6 +205,7 @@ class OperationalStateDatabaseOwner {
if (options.schemaMigration === 'require_current' && !existsSync(databasePath)) {
throw new OperationalStateMigrationBlockedError(
new Error('Operational state has not been initialized by its Runtime Host'),
+ 'requires_host_migration',
);
}
mkdirSync(dirname(databasePath), { recursive: true });
@@ -291,7 +295,10 @@ function requireCurrentOperationalState(database: DatabaseSync): void {
try {
const inspection = inspectOperationalStateSchema(database);
if (inspection.status === 'current' && isCurrentOperationalTargetSchema(database)) return;
- throw new Error('Operational state requires migration by its Runtime Host');
+ throw new OperationalStateMigrationBlockedError(
+ new Error('Operational state requires migration by its Runtime Host'),
+ 'requires_host_migration',
+ );
} catch (error) {
if (isSqliteEnvironmentError(error)) throw error;
if (error instanceof OperationalStateMigrationBlockedError) throw error;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org