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/04 11:30:56 UTC
(maka) branch fix/stale-ephemeral-host-recovery created (now 162b5096cd)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a change to branch fix/stale-ephemeral-host-recovery
in repository https://gitbox.apache.org/repos/asf/maka.git
at 162b5096cd fix(desktop): stop stale ephemeral Host at startup
This branch includes the following new commits:
new 162b5096cd fix(desktop): stop stale ephemeral Host at startup
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/01: fix(desktop): stop stale ephemeral Host at startup
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch fix/stale-ephemeral-host-recovery
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 162b5096cd07a8c690ca633d10e94007de673fa7
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Sep 4 19:29:02 2026 +0800
fix(desktop): stop stale ephemeral Host at startup
Offer an explicit recovery action when an incompatible local ephemeral Runtime Host still owns the State Root. Revalidate the exact root, Host epoch, PID, lifecycle, and live Desktop intent immediately before forced termination; managed and remote Hosts retain their existing authority boundaries.
Generated-by: OpenAI Codex
---
.../__tests__/runtime-host-desktop-manager.test.ts | 56 +++++++++++++++++++++-
.../src/main/runtime-host-desktop-manager.ts | 41 +++++++++++++++-
.../src/client/registered-host-termination.ts | 12 ++---
3 files changed, 101 insertions(+), 8 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
index ad5d984426..b8dda7f511 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
@@ -1494,6 +1494,60 @@ test('waits passively for a Host that cannot be taken over', async () => {
await owner.close();
});
+test('offers to stop an exact local ephemeral Host before retrying startup', async () => {
+ const observed = incompatibleHost('blocked_by_residency');
+ const conflict = {
+ ...observed,
+ registration: { ...observed.registration, lifecycleMode: 'ephemeral' as const },
+ handshake: {
+ ...observed.handshake,
+ activity: {
+ connections: 0,
+ activeOperations: 0,
+ processUptimeSeconds: 60,
+ residencies: [],
+ },
+ },
+ };
+ const replacement = candidateHarness();
+ let starts = 0;
+ let prompts = 0;
+ let terminations = 0;
+ const owner = await startRuntimeHostDesktopManager(
+ { rootPath: '/workspace' } as DesktopRuntimeHostCandidateStartInput,
+ {
+ startCandidate: async () => {
+ starts += 1;
+ return starts === 1 ? conflict : ready(replacement.candidate);
+ },
+ upgradePrompts: {
+ restartable: async () => assert.fail('incompatible Host used restart prompt'),
+ nonRestartable: async (_conflict, action) => {
+ prompts += 1;
+ assert.equal(action, 'replace_may_interrupt_work');
+ return 'replace';
+ },
+ },
+ forceTerminateHost: async (identity, stillAuthorized) => {
+ terminations += 1;
+ assert.deepEqual(identity, {
+ rootPath: '/workspace',
+ rootId: conflict.registration.rootId,
+ hostEpoch: conflict.registration.hostEpoch,
+ pid: conflict.registration.pid,
+ });
+ assert.equal(stillAuthorized(), true);
+ return true;
+ },
+ },
+ );
+
+ assert.equal(prompts, 1, 'even an idle snapshot must not authorize a forced stop');
+ assert.equal(terminations, 1);
+ assert.equal(starts, 2);
+ await owner.close();
+});
+
test('silently replaces an idle non-restartable Local Host and retries', async () => {
const observed = upgradeRequired(true);
const conflict = {
@@ -1667,7 +1721,7 @@ test('lets the user cancel startup when an incompatible Host owns the root', asy
function incompatibleHost(
replacement: 'wait_for_idle_exit' | 'blocked_by_residency',
-): DesktopRuntimeHostCandidateStartResult {
+): Extract<DesktopRuntimeHostCandidateStartResult, { kind: 'incompatible' }> {
return {
kind: 'incompatible',
registration: hostRegistration({ compatibilityEpoch: RUNTIME_HOST_COMPATIBILITY_EPOCH - 1 }),
diff --git a/apps/desktop/src/main/runtime-host-desktop-manager.ts b/apps/desktop/src/main/runtime-host-desktop-manager.ts
index 6731a4aae2..8bf607b60f 100644
--- a/apps/desktop/src/main/runtime-host-desktop-manager.ts
+++ b/apps/desktop/src/main/runtime-host-desktop-manager.ts
@@ -1138,7 +1138,8 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
) {
const replacement = target.input.profileTarget
? undefined
- : await this.resolveLocalHostReplacement?.(result.registration, signal);
+ : this.#registeredEphemeralHostReplacement(target, result.registration, signal) ??
+ (await this.resolveLocalHostReplacement?.(result.registration, signal));
const activity = result.handshake?.activity;
if (replacement && activity && isHostActivityIdle(activity)) {
// Only a complete, observed snapshot can authorize silent
@@ -1184,6 +1185,44 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
}
}
+ #registeredEphemeralHostReplacement(
+ target: DesktopRuntimeHostTargetGeneration,
+ registration: HostRegistration,
+ signal: AbortSignal,
+ ): RuntimeHostLocalReplacement | undefined {
+ if (registration.lifecycleMode !== 'ephemeral') return undefined;
+ const stillAuthorized = () =>
+ !this.#closed &&
+ !signal.aborted &&
+ target.valid &&
+ this.#targets.get(target.target.profile.id) === target;
+ return {
+ replace: async (activeWorkPolicy) => {
+ // This Host cannot participate in the current retirement protocol, so
+ // an earlier idle snapshot cannot prove that it remains idle. Require
+ // explicit consent before using the identity-fenced termination path.
+ if (activeWorkPolicy === 'refuse_active_work') return 'active_tasks';
+ signal.throwIfAborted();
+ const terminated = await this.forceTerminateHost(
+ {
+ rootPath: this.#baseInput.rootPath,
+ rootId: registration.rootId,
+ hostEpoch: registration.hostEpoch,
+ pid: registration.pid,
+ },
+ stillAuthorized,
+ );
+ signal.throwIfAborted();
+ if (!terminated) {
+ throw new RuntimeHostPermanentReconnectError(
+ 'The older Runtime Host changed before it could be stopped safely',
+ );
+ }
+ return 'replaced';
+ },
+ };
+ }
+
#resolveRestartable(
conflict: RuntimeHostRestartableConflict,
): Promise<RuntimeHostRestartDecision> {
diff --git a/packages/runtime-host/src/client/registered-host-termination.ts b/packages/runtime-host/src/client/registered-host-termination.ts
index 4832b08c99..cf9e58aca1 100644
--- a/packages/runtime-host/src/client/registered-host-termination.ts
+++ b/packages/runtime-host/src/client/registered-host-termination.ts
@@ -49,25 +49,25 @@ const defaultDependencies: RegisteredRuntimeHostTerminationDependencies = {
/**
* Force-terminates only the exact local ephemeral Host still registered for
* the expected State Root. Callers must reserve this for explicit recovery
- * after graceful retirement fails.
+ * and keep their authorization current until the signal is sent.
*/
export function forceTerminateRegisteredRuntimeHost(
identity: RegisteredRuntimeHostIdentity,
- stillOwnsProcess: () => boolean,
+ stillAuthorized: () => boolean,
): Promise<boolean> {
return forceTerminateRegisteredRuntimeHostWithDependencies(
identity,
- stillOwnsProcess,
+ stillAuthorized,
defaultDependencies,
);
}
export async function forceTerminateRegisteredRuntimeHostWithDependencies(
identity: RegisteredRuntimeHostIdentity,
- stillOwnsProcess: () => boolean,
+ stillAuthorized: () => boolean,
dependencies: RegisteredRuntimeHostTerminationDependencies,
): Promise<boolean> {
- if (!stillOwnsProcess()) return false;
+ if (!stillAuthorized()) return false;
const capability = await resolveStorageRoot({ path: identity.rootPath, kind: 'interactive' });
if (capability.rootId !== identity.rootId) return false;
const { controlDirectory } = await prepareStorageRootControlDirectory(capability);
@@ -86,7 +86,7 @@ export async function forceTerminateRegisteredRuntimeHostWithDependencies(
// before the OS signal, so neither a successor nor a reused PID can
// inherit stale intent.
signalTarget = await readHostRegistration(controlDirectory);
- return matchesIdentity(signalTarget, identity) && stillOwnsProcess();
+ return matchesIdentity(signalTarget, identity) && stillAuthorized();
},
fallback: () => {
try {
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org