@cydm/happy-elves 0.1.0-beta.74 → 0.1.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +22 -1
- package/apps/cli/dist/commands/lib/bootstrap-daemon.js +73 -1
- package/apps/cli/dist/commands/lib/bootstrap-device-recovery.d.ts +5 -2
- package/apps/cli/dist/commands/lib/bootstrap-device-recovery.js +42 -51
- package/apps/cli/dist/commands/lib/bootstrap.js +14 -13
- package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +24 -0
- package/apps/daemon/dist/relay/connection.js +5 -1
- package/apps/daemon/dist/relay/register.js +1 -0
- package/apps/daemon/dist/relay/send.js +1 -0
- package/apps/daemon/dist/runtime/external-provider.js +7 -0
- package/apps/daemon/dist/session/lifecycle.d.ts +4 -1
- package/apps/daemon/dist/session/lifecycle.js +156 -20
- package/apps/daemon/dist/session/metadata.d.ts +2 -1
- package/apps/daemon/dist/session/metadata.js +10 -1
- package/apps/daemon/dist/session/prompt.js +58 -7
- package/apps/daemon/dist/turn-coordinator.d.ts +1 -1
- package/apps/daemon/dist/types.d.ts +2 -0
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/controller-handlers.js +91 -4
- package/apps/relay/dist/db.js +2 -0
- package/apps/relay/dist/machine-command-result-handlers.js +59 -4
- package/apps/relay/dist/machine-handler-context.d.ts +7 -0
- package/apps/relay/dist/projections.js +5 -0
- package/apps/relay/dist/relay-context.js +7 -2
- package/apps/relay/dist/types.d.ts +1 -0
- package/apps/relay/dist/websocket.js +1 -0
- package/build-identity.json +2 -2
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +1 -0
- package/packages/client/dist/client.js +9 -0
- package/packages/client/dist/types.d.ts +2 -0
- package/packages/runtime/dist/index.d.ts +14 -0
- package/packages/runtime-cli/dist/codex-app-server.d.ts +5 -0
- package/packages/runtime-cli/dist/codex-app-server.js +126 -0
- package/packages/runtime-cli/dist/index.js +8 -0
- package/packages/runtime-cli/dist/session-store.d.ts +1 -0
- package/packages/runtime-cli/dist/session-store.js +32 -0
- package/packages/shared/dist/protocol-schemas.d.ts +5 -0
- package/packages/shared/dist/protocol-schemas.js +4 -0
- package/packages/shared/dist/protocol-types.d.ts +4 -0
- package/packages/shared/dist/protocol.d.ts +16 -0
- package/packages/shared/dist/protocol.js +20 -0
- package/packages/shared/dist/session-state.d.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ControllerClient, type ControllerSnapshot } from "../../../../../packages/client/dist/index.js";
|
|
2
2
|
import type { MachineSnapshot } from "../../../../../packages/shared/dist/index.js";
|
|
3
|
-
import { localDaemonStatus, readLocalDaemonPidIdentity, type SpawnedDaemonHandle } from "./local-daemon.js";
|
|
3
|
+
import { inspectLocalDaemonRuntime, localDaemonStatus, readLocalDaemonPidIdentity, stopLocalDaemon, stopSpawnedDaemon, type SpawnedDaemonHandle } from "./local-daemon.js";
|
|
4
4
|
import type { DaemonConfig } from "./types.js";
|
|
5
5
|
export declare function startFailureMayUseCommittedConfig(error: unknown): boolean;
|
|
6
6
|
export declare function ensureDaemonRunning(expectedConfig: DaemonConfig, cwd?: string): Promise<{
|
|
@@ -12,6 +12,27 @@ export declare function ensureDaemonRunning(expectedConfig: DaemonConfig, cwd?:
|
|
|
12
12
|
committedConfigMayBeInUse?: boolean;
|
|
13
13
|
autostartNeedsAttention?: string;
|
|
14
14
|
}>;
|
|
15
|
+
type BootstrapDaemonRestartOptions = {
|
|
16
|
+
readStatus?: typeof localDaemonStatus;
|
|
17
|
+
inspectRuntime?: typeof inspectLocalDaemonRuntime;
|
|
18
|
+
readIdentity?: typeof readLocalDaemonPidIdentity;
|
|
19
|
+
stopLocal?: typeof stopLocalDaemon;
|
|
20
|
+
stopStarted?: typeof stopSpawnedDaemon;
|
|
21
|
+
ensureRunning?: typeof ensureDaemonRunning;
|
|
22
|
+
assertConfigFence?: (expectedConfig: DaemonConfig) => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* `happy-elves start` is an explicit foreground recovery/entry command. When
|
|
26
|
+
* it finds a verified local Daemon, restart that exact owner so the process
|
|
27
|
+
* always loads the currently installed package before the browser opens.
|
|
28
|
+
*
|
|
29
|
+
* The lower-level `ensureDaemonRunning()` deliberately remains idempotent for
|
|
30
|
+
* `happy-elves daemon start`, enrollment rollback, and service recovery.
|
|
31
|
+
*/
|
|
32
|
+
export declare function restartDaemonForBootstrap(expectedConfig: DaemonConfig, cwd?: string, options?: BootstrapDaemonRestartOptions): Promise<Awaited<ReturnType<typeof ensureDaemonRunning>> & {
|
|
33
|
+
restarted: boolean;
|
|
34
|
+
previousInstanceGeneration?: string;
|
|
35
|
+
}>;
|
|
15
36
|
export type DaemonReadinessResult = {
|
|
16
37
|
phase: "relay-authenticated" | "process-exited" | "owner-changed" | "relay-pending";
|
|
17
38
|
local: Awaited<ReturnType<typeof localDaemonStatus>>;
|
|
@@ -3,7 +3,7 @@ import { daemonRuntimeConfigFingerprint } from "../../../../../packages/shared/d
|
|
|
3
3
|
import { CliError } from "../../errors.js";
|
|
4
4
|
import { startDaemonTimeoutMs } from "./paths.js";
|
|
5
5
|
import { readDaemonConfig } from "./config.js";
|
|
6
|
-
import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, inspectLocalDaemonRuntime, ensureRunningDaemonAutostart, localDaemonStatus, readLocalDaemonPidIdentity, spawnDaemonStartTracked, stopSpawnedDaemon, } from "./local-daemon.js";
|
|
6
|
+
import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, inspectLocalDaemonRuntime, ensureRunningDaemonAutostart, localDaemonStatus, readLocalDaemonPidIdentity, spawnDaemonStartTracked, stopLocalDaemon, stopSpawnedDaemon, } from "./local-daemon.js";
|
|
7
7
|
const committedConfigMayBeInUseErrors = new WeakSet();
|
|
8
8
|
export function startFailureMayUseCommittedConfig(error) {
|
|
9
9
|
return Boolean(error && typeof error === "object" && committedConfigMayBeInUseErrors.has(error));
|
|
@@ -183,6 +183,78 @@ export async function ensureDaemonRunning(expectedConfig, cwd = process.cwd()) {
|
|
|
183
183
|
throw error;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* `happy-elves start` is an explicit foreground recovery/entry command. When
|
|
188
|
+
* it finds a verified local Daemon, restart that exact owner so the process
|
|
189
|
+
* always loads the currently installed package before the browser opens.
|
|
190
|
+
*
|
|
191
|
+
* The lower-level `ensureDaemonRunning()` deliberately remains idempotent for
|
|
192
|
+
* `happy-elves daemon start`, enrollment rollback, and service recovery.
|
|
193
|
+
*/
|
|
194
|
+
export async function restartDaemonForBootstrap(expectedConfig, cwd = process.cwd(), options = {}) {
|
|
195
|
+
const readStatus = options.readStatus ?? localDaemonStatus;
|
|
196
|
+
const inspectRuntime = options.inspectRuntime ?? inspectLocalDaemonRuntime;
|
|
197
|
+
const readIdentity = options.readIdentity ?? readLocalDaemonPidIdentity;
|
|
198
|
+
const stopLocal = options.stopLocal ?? stopLocalDaemon;
|
|
199
|
+
const stopStarted = options.stopStarted ?? stopSpawnedDaemon;
|
|
200
|
+
const ensureRunning = options.ensureRunning ?? ensureDaemonRunning;
|
|
201
|
+
const assertConfigFence = options.assertConfigFence ?? assertDaemonConfigFence;
|
|
202
|
+
await assertConfigFence(expectedConfig);
|
|
203
|
+
const current = await readStatus();
|
|
204
|
+
assertLocalDaemonVerified(current);
|
|
205
|
+
if (!current.running) {
|
|
206
|
+
return {
|
|
207
|
+
...await ensureRunning(expectedConfig, cwd),
|
|
208
|
+
restarted: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const owner = await readIdentity();
|
|
212
|
+
if (!owner.record || owner.record.pid !== current.pid) {
|
|
213
|
+
throw new CliError("The verified Daemon owner identity changed before restart; no process was signalled.", "DAEMON_PID_INVALID");
|
|
214
|
+
}
|
|
215
|
+
const runtime = await inspectRuntime(expectedConfig);
|
|
216
|
+
if (runtime.state !== "match" && runtime.state !== "missing") {
|
|
217
|
+
const code = runtime.state === "invalid"
|
|
218
|
+
? "DAEMON_RUNTIME_IDENTITY_INVALID"
|
|
219
|
+
: runtime.state === "owner-mismatch"
|
|
220
|
+
? "DAEMON_RUNTIME_IDENTITY_MISMATCH"
|
|
221
|
+
: "DAEMON_RUNTIME_CONFIG_MISMATCH";
|
|
222
|
+
throw new CliError("The running Daemon identity could not be matched to daemon.json, so happy-elves start did not restart or signal it.", code);
|
|
223
|
+
}
|
|
224
|
+
if (runtime.state === "match" &&
|
|
225
|
+
(runtime.identity.pid !== owner.record.pid ||
|
|
226
|
+
runtime.identity.instanceNonce !== owner.record.instanceNonce ||
|
|
227
|
+
runtime.identity.startedAt !== owner.record.startedAt)) {
|
|
228
|
+
throw new CliError("The Daemon runtime owner changed while restart was being verified; no process was signalled.", "DAEMON_RUNTIME_IDENTITY_MISMATCH");
|
|
229
|
+
}
|
|
230
|
+
const previousInstanceGeneration = owner.record.instanceNonce;
|
|
231
|
+
await assertConfigFence(expectedConfig);
|
|
232
|
+
const stopped = await stopLocal(startDaemonTimeoutMs, {
|
|
233
|
+
pid: owner.record.pid,
|
|
234
|
+
instanceNonce: owner.record.instanceNonce,
|
|
235
|
+
startedAt: owner.record.startedAt,
|
|
236
|
+
});
|
|
237
|
+
if (stopped.running || stopped.alive) {
|
|
238
|
+
throw new CliError("Daemon did not stop before the restart deadline; no replacement process was started.", "DAEMON_STOP_FAILED");
|
|
239
|
+
}
|
|
240
|
+
const started = await ensureRunning(expectedConfig, cwd);
|
|
241
|
+
if (started.instanceGeneration === previousInstanceGeneration) {
|
|
242
|
+
if (started.startedHandle) {
|
|
243
|
+
try {
|
|
244
|
+
await stopStarted(started.startedHandle);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
throw new CliError(`Daemon restart returned a stale generation and its replacement cleanup failed: ${error instanceof Error ? error.message : String(error)}`, "DAEMON_START_CLEANUP_FAILED");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw new CliError("Daemon restart did not publish a fresh process generation.", "DAEMON_RESTART_GENERATION_MISMATCH");
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
...started,
|
|
254
|
+
restarted: true,
|
|
255
|
+
previousInstanceGeneration,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
186
258
|
function committedConfigInUseError(message) {
|
|
187
259
|
return markCommittedConfigMayBeInUse(new CliError(message, "DAEMON_START_CONFLICT"));
|
|
188
260
|
}
|
|
@@ -2,7 +2,7 @@ import { ControllerClient } from "../../../../../packages/client/dist/index.js";
|
|
|
2
2
|
import { type DeviceRecoveryIntentV1 } from "../../../../../packages/shared/dist/index.js";
|
|
3
3
|
import { stopSpawnedDaemon } from "./local-daemon.js";
|
|
4
4
|
import { type StartConfigPreflight } from "./bootstrap-config.js";
|
|
5
|
-
import { ensureDaemonRunning } from "./bootstrap-daemon.js";
|
|
5
|
+
import { type ensureDaemonRunning } from "./bootstrap-daemon.js";
|
|
6
6
|
export declare function startDeviceRecoveryBootstrap(params: {
|
|
7
7
|
flags: Record<string, string | boolean>;
|
|
8
8
|
preflight: StartConfigPreflight;
|
|
@@ -12,12 +12,15 @@ export declare function startDeviceRecoveryBootstrap(params: {
|
|
|
12
12
|
}): Promise<void>;
|
|
13
13
|
export declare function deviceRecoveryIntentNeedsReplacement(intent: Pick<DeviceRecoveryIntentV1, "status">): boolean;
|
|
14
14
|
export declare function isTerminalRecoveryIntentCreationFailure(error: unknown): boolean;
|
|
15
|
-
export declare function projectRecoveredDaemonReadinessFailure(started: Awaited<ReturnType<typeof ensureDaemonRunning
|
|
15
|
+
export declare function projectRecoveredDaemonReadinessFailure(started: Awaited<ReturnType<typeof ensureDaemonRunning>> & {
|
|
16
|
+
restarted?: boolean;
|
|
17
|
+
}, machineName: string, readinessError: unknown, options?: {
|
|
16
18
|
stopStarted?: typeof stopSpawnedDaemon;
|
|
17
19
|
}): Promise<{
|
|
18
20
|
online: false;
|
|
19
21
|
pid?: number;
|
|
20
22
|
started: boolean;
|
|
23
|
+
restarted: boolean;
|
|
21
24
|
name: string;
|
|
22
25
|
needsAttention: string;
|
|
23
26
|
}>;
|
|
@@ -7,10 +7,10 @@ import { accountFingerprintV1, deviceRecoveryTtlMsV1, randomId, } from "../../..
|
|
|
7
7
|
import { daemonRuntimeConfigFingerprint } from "../../../../../packages/shared/dist/node/daemon-process.js";
|
|
8
8
|
import { CliError } from "../../errors.js";
|
|
9
9
|
import { ok } from "./json.js";
|
|
10
|
-
import { assertLocalDaemonVerified,
|
|
10
|
+
import { assertLocalDaemonVerified, localDaemonStatus, stopSpawnedDaemon, } from "./local-daemon.js";
|
|
11
11
|
import { withAccountConfigTransaction } from "./config.js";
|
|
12
12
|
import { controllerBaseUrl } from "./bootstrap-config.js";
|
|
13
|
-
import {
|
|
13
|
+
import { readDaemonProjectionBaseline, restartDaemonForBootstrap, waitForDaemonReadiness, } from "./bootstrap-daemon.js";
|
|
14
14
|
import { openUrl } from "./bootstrap-output.js";
|
|
15
15
|
import { assertNoPendingDaemonEnrollmentForDeviceRecovery } from "./daemon-enrollment-intent.js";
|
|
16
16
|
import { configDir, configPath, deviceRecoveryPendingPath, startDaemonTimeoutMs } from "./paths.js";
|
|
@@ -318,34 +318,10 @@ async function ensureRecoveredDaemonReady(params) {
|
|
|
318
318
|
...projection,
|
|
319
319
|
...(current.pid ? { pid: current.pid } : {}),
|
|
320
320
|
started: false,
|
|
321
|
+
restarted: false,
|
|
321
322
|
needsAttention: error instanceof Error ? error.message : "The local Daemon owner needs verification.",
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
|
-
if (current.running) {
|
|
325
|
-
const runtime = await inspectLocalDaemonRuntime(params.daemonConfig);
|
|
326
|
-
if (runtime.state !== "match") {
|
|
327
|
-
const projection = await readProjection();
|
|
328
|
-
const needsAttention = runtime.state === "missing"
|
|
329
|
-
? "Automatic startup metadata will be repaired on the next controlled Daemon restart."
|
|
330
|
-
: "The running Daemon identity needs repair before Happy Elves can manage its process.";
|
|
331
|
-
return {
|
|
332
|
-
...projection,
|
|
333
|
-
...(current.pid ? { pid: current.pid } : {}),
|
|
334
|
-
started: false,
|
|
335
|
-
needsAttention,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
const autostart = await ensureRunningDaemonAutostart();
|
|
339
|
-
const projection = await readProjection();
|
|
340
|
-
return {
|
|
341
|
-
...projection,
|
|
342
|
-
...(current.pid ? { pid: current.pid } : {}),
|
|
343
|
-
started: false,
|
|
344
|
-
...(autostart.state === "needs-attention"
|
|
345
|
-
? { needsAttention: autostart.message }
|
|
346
|
-
: {}),
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
325
|
let baseline;
|
|
350
326
|
try {
|
|
351
327
|
baseline = await readDaemonProjectionBaseline(client, params.daemonConfig.machineId, {
|
|
@@ -357,37 +333,51 @@ async function ensureRecoveredDaemonReady(params) {
|
|
|
357
333
|
}
|
|
358
334
|
let started;
|
|
359
335
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
336
|
+
const ready = await withAccountConfigTransaction(async (transaction) => {
|
|
337
|
+
const currentDaemonConfig = await transaction.readDaemon();
|
|
338
|
+
if (!currentDaemonConfig ||
|
|
339
|
+
daemonRuntimeConfigFingerprint(currentDaemonConfig) !== daemonRuntimeConfigFingerprint(params.daemonConfig)) {
|
|
340
|
+
throw new CliError("The local Daemon identity changed before restart. No process was signalled.", "CONFIG_CHANGED_DURING_START");
|
|
341
|
+
}
|
|
342
|
+
started = await restartDaemonForBootstrap(params.daemonConfig, params.cwd);
|
|
343
|
+
const readiness = await waitForDaemonReadiness(client, params.daemonConfig.machineId, {
|
|
344
|
+
baseline,
|
|
345
|
+
expectedInstanceGeneration: started.instanceGeneration,
|
|
346
|
+
requireFreshProjection: started.started || started.restarted,
|
|
347
|
+
timeoutMs: startDaemonTimeoutMs,
|
|
348
|
+
});
|
|
349
|
+
if (readiness.phase !== "relay-authenticated" || !readiness.machine?.online) {
|
|
350
|
+
throw new CliError("The Daemon did not authenticate before the startup deadline.", "DAEMON_ONLINE_TIMEOUT");
|
|
351
|
+
}
|
|
352
|
+
const confirmedDaemonConfig = await transaction.readDaemon();
|
|
353
|
+
if (!confirmedDaemonConfig ||
|
|
354
|
+
daemonRuntimeConfigFingerprint(confirmedDaemonConfig) !== daemonRuntimeConfigFingerprint(params.daemonConfig)) {
|
|
355
|
+
throw new CliError("The local Daemon identity changed during restart.", "CONFIG_CHANGED_DURING_START");
|
|
356
|
+
}
|
|
357
|
+
return { started, readiness };
|
|
377
358
|
});
|
|
378
|
-
|
|
379
|
-
throw new CliError("The Daemon did not authenticate before the startup deadline.", "DAEMON_ONLINE_TIMEOUT");
|
|
380
|
-
}
|
|
381
|
-
const pid = started.local.pid ?? started.startedPid;
|
|
359
|
+
const pid = ready.started.local.pid ?? ready.started.startedPid;
|
|
382
360
|
return {
|
|
383
361
|
online: true,
|
|
384
362
|
...(pid ? { pid } : {}),
|
|
385
|
-
started: started.started,
|
|
386
|
-
|
|
363
|
+
started: ready.started.started,
|
|
364
|
+
restarted: ready.started.restarted,
|
|
365
|
+
name: ready.readiness.machine?.name ?? params.daemonConfig.machineName,
|
|
387
366
|
};
|
|
388
367
|
}
|
|
389
368
|
catch (error) {
|
|
390
|
-
|
|
369
|
+
if (started) {
|
|
370
|
+
return await projectRecoveredDaemonReadinessFailure(started, params.daemonConfig.machineName, error);
|
|
371
|
+
}
|
|
372
|
+
const settled = await localDaemonStatus().catch(() => current);
|
|
373
|
+
return {
|
|
374
|
+
online: false,
|
|
375
|
+
name: params.daemonConfig.machineName,
|
|
376
|
+
...(settled.pid ? { pid: settled.pid } : {}),
|
|
377
|
+
started: false,
|
|
378
|
+
restarted: current.running && !settled.running,
|
|
379
|
+
needsAttention: error instanceof Error ? error.message : "The Daemon could not be restarted automatically.",
|
|
380
|
+
};
|
|
391
381
|
}
|
|
392
382
|
}
|
|
393
383
|
export async function projectRecoveredDaemonReadinessFailure(started, machineName, readinessError, options = {}) {
|
|
@@ -408,6 +398,7 @@ export async function projectRecoveredDaemonReadinessFailure(started, machineNam
|
|
|
408
398
|
online: false,
|
|
409
399
|
...(started.local.pid ?? started.startedPid ? { pid: started.local.pid ?? started.startedPid } : {}),
|
|
410
400
|
started: daemonCleanupError ? started.started : false,
|
|
401
|
+
restarted: started.restarted === true,
|
|
411
402
|
name: machineName,
|
|
412
403
|
needsAttention,
|
|
413
404
|
};
|
|
@@ -10,7 +10,7 @@ import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, inspe
|
|
|
10
10
|
import { redactedRelayUrl, withAccountConfigTransaction, } from "./config.js";
|
|
11
11
|
import { cliErrorFromFetch } from "./relay-http.js";
|
|
12
12
|
import { controllerBaseUrl, ensureControllerConfig, ensureDaemonPaired, readStartConfigPreflight } from "./bootstrap-config.js";
|
|
13
|
-
import {
|
|
13
|
+
import { readDaemonProjectionBaseline, restartDaemonForBootstrap, startFailureMayUseCommittedConfig, waitForDaemonReadiness, } from "./bootstrap-daemon.js";
|
|
14
14
|
import { openUrl, writeHumanStartOutput } from "./bootstrap-output.js";
|
|
15
15
|
import { startJoinBootstrap } from "./bootstrap-join.js";
|
|
16
16
|
import { reconcileSatisfiedDeviceRecoveryPending, startDeviceRecoveryBootstrap } from "./bootstrap-device-recovery.js";
|
|
@@ -85,16 +85,15 @@ export async function startBootstrap(flags) {
|
|
|
85
85
|
if (daemonBefore.running && preflight.kind !== "ready") {
|
|
86
86
|
throw new CliError("A verified daemon is running, but the matching controller/daemon config pair is incomplete. No configuration or daemon process was changed. Run happy-elves doctor before recovering this workspace.", "CONFIG_SPLIT_BRAIN");
|
|
87
87
|
}
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
88
|
+
// Top-level `happy-elves start` deliberately restarts a verified owner so
|
|
89
|
+
// it always loads the currently installed package. A missing marker is the
|
|
90
|
+
// supported legacy-upgrade case: exact PID v2 ownership still fences the
|
|
91
|
+
// stop, and the replacement publishes a fresh marker. Invalid, mismatched,
|
|
92
|
+
// or unverified identities remain fail-closed and are never signalled.
|
|
92
93
|
if (!daemonProcessNeedsAttention && daemonBefore.running && preflight.daemon) {
|
|
93
94
|
const runtime = await inspectLocalDaemonRuntime(preflight.daemon);
|
|
94
|
-
if (runtime.state !== "match") {
|
|
95
|
-
daemonProcessNeedsAttention =
|
|
96
|
-
? "Automatic startup metadata will be repaired on the next controlled Daemon restart."
|
|
97
|
-
: "The running Daemon identity needs repair before Happy Elves can manage its process.";
|
|
95
|
+
if (runtime.state !== "match" && runtime.state !== "missing") {
|
|
96
|
+
daemonProcessNeedsAttention = "The running Daemon identity needs repair before Happy Elves can manage its process.";
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
const networkDeadline = Date.now() + startDaemonTimeoutMs;
|
|
@@ -187,19 +186,20 @@ export async function startBootstrap(flags) {
|
|
|
187
186
|
started: false,
|
|
188
187
|
startedPid: undefined,
|
|
189
188
|
committedConfigMayBeInUse: daemonBefore.running,
|
|
189
|
+
restarted: false,
|
|
190
190
|
},
|
|
191
191
|
machine: baseline,
|
|
192
192
|
invite,
|
|
193
193
|
daemonProcessNeedsAttention,
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
|
-
const daemon = await
|
|
196
|
+
const daemon = await restartDaemonForBootstrap(daemonState.config, cwd);
|
|
197
197
|
startedHandle = daemon.startedHandle;
|
|
198
198
|
committedConfigMayBeInUse = daemon.committedConfigMayBeInUse === true;
|
|
199
199
|
const readiness = await waitForDaemonReadiness(client, daemonState.config.machineId, {
|
|
200
200
|
baseline,
|
|
201
201
|
expectedInstanceGeneration: daemon.instanceGeneration,
|
|
202
|
-
requireFreshProjection: daemon.started,
|
|
202
|
+
requireFreshProjection: daemon.started || daemon.restarted,
|
|
203
203
|
// Lifetime-lock acquisition/reclaim and relay-authenticated readiness
|
|
204
204
|
// are separate bounded phases.
|
|
205
205
|
timeoutMs: startDaemonTimeoutMs,
|
|
@@ -317,8 +317,9 @@ export async function startBootstrap(flags) {
|
|
|
317
317
|
pid: daemon.local.pid ?? daemon.startedPid,
|
|
318
318
|
pidPath: daemon.local.pidPath,
|
|
319
319
|
started: daemon.started,
|
|
320
|
-
alreadyRunning:
|
|
321
|
-
|
|
320
|
+
alreadyRunning: daemon.restarted !== true &&
|
|
321
|
+
(daemonBefore.running || daemon.committedConfigMayBeInUse === true),
|
|
322
|
+
restarted: daemon.restarted,
|
|
322
323
|
...(daemonNeedsAttention ? { needsAttention: daemonNeedsAttention } : {}),
|
|
323
324
|
},
|
|
324
325
|
...(invite ? {
|
|
@@ -17833,6 +17833,7 @@ var capabilitiesSchema = external_exports.object({
|
|
|
17833
17833
|
memoryCommandsV1: external_exports.boolean().optional(),
|
|
17834
17834
|
memoryProjectsV1: external_exports.boolean().optional(),
|
|
17835
17835
|
gatewayCommandsV1: external_exports.boolean().optional(),
|
|
17836
|
+
sessionProviderRestoreV1: external_exports.boolean().optional(),
|
|
17836
17837
|
filePreview: external_exports.boolean().optional(),
|
|
17837
17838
|
scheduleAuthContextV1: external_exports.boolean().optional(),
|
|
17838
17839
|
scheduleCreateSemanticDigestV1: external_exports.boolean().optional(),
|
|
@@ -17877,6 +17878,9 @@ var sessionSnapshotSchema = external_exports.object({
|
|
|
17877
17878
|
}).optional(),
|
|
17878
17879
|
sourceSessionId: external_exports.string().min(1).optional(),
|
|
17879
17880
|
sourceCheckpointId: external_exports.string().min(1).optional(),
|
|
17881
|
+
recovery: external_exports.object({
|
|
17882
|
+
code: external_exports.literal("provider-archived")
|
|
17883
|
+
}).optional(),
|
|
17880
17884
|
historyModel: external_exports.enum(["append-only", "authoritative-linear"]).optional(),
|
|
17881
17885
|
capabilities: sessionCapabilitiesSchema.optional()
|
|
17882
17886
|
});
|
|
@@ -18198,6 +18202,11 @@ var controllerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
18198
18202
|
requestId: external_exports.string().min(1),
|
|
18199
18203
|
sessionId: external_exports.string().min(1)
|
|
18200
18204
|
}),
|
|
18205
|
+
external_exports.object({
|
|
18206
|
+
type: external_exports.literal("controller:restoreSession"),
|
|
18207
|
+
requestId: external_exports.string().min(1),
|
|
18208
|
+
sessionId: external_exports.string().min(1)
|
|
18209
|
+
}),
|
|
18201
18210
|
external_exports.object({
|
|
18202
18211
|
type: external_exports.literal("controller:listHistoricalSessions"),
|
|
18203
18212
|
requestId: external_exports.string().min(1),
|
|
@@ -18442,6 +18451,14 @@ var machineMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
18442
18451
|
messageId: external_exports.string().min(1).optional(),
|
|
18443
18452
|
encryptedMetadata: encryptedEnvelopeSchema.optional()
|
|
18444
18453
|
}),
|
|
18454
|
+
external_exports.object({
|
|
18455
|
+
type: external_exports.literal("machine:sessionRestored"),
|
|
18456
|
+
requestId: external_exports.string().min(1),
|
|
18457
|
+
sessionId: external_exports.string().min(1),
|
|
18458
|
+
name: external_exports.string().min(1).optional(),
|
|
18459
|
+
messageId: external_exports.string().min(1).optional(),
|
|
18460
|
+
encryptedMetadata: encryptedEnvelopeSchema.optional()
|
|
18461
|
+
}),
|
|
18445
18462
|
external_exports.object({
|
|
18446
18463
|
type: external_exports.literal("machine:historicalSessions"),
|
|
18447
18464
|
requestId: external_exports.string().min(1),
|
|
@@ -18669,6 +18686,12 @@ var machineCommandSchema = external_exports.discriminatedUnion("type", [
|
|
|
18669
18686
|
sessionId: external_exports.string().min(1),
|
|
18670
18687
|
encryptedMetadata: encryptedEnvelopeSchema.optional()
|
|
18671
18688
|
}),
|
|
18689
|
+
external_exports.object({
|
|
18690
|
+
type: external_exports.literal("machine:restoreSession"),
|
|
18691
|
+
requestId: external_exports.string().min(1),
|
|
18692
|
+
sessionId: external_exports.string().min(1),
|
|
18693
|
+
encryptedMetadata: encryptedEnvelopeSchema.optional()
|
|
18694
|
+
}),
|
|
18672
18695
|
external_exports.object({
|
|
18673
18696
|
type: external_exports.literal("machine:listHistoricalSessions"),
|
|
18674
18697
|
requestId: external_exports.string().min(1),
|
|
@@ -18791,6 +18814,7 @@ var _machineClientMessageTypeSchema = external_exports.enum([
|
|
|
18791
18814
|
"machine:sessionHeadAdvanced",
|
|
18792
18815
|
"machine:turnDone",
|
|
18793
18816
|
"machine:sessionResumed",
|
|
18817
|
+
"machine:sessionRestored",
|
|
18794
18818
|
"machine:historicalSessions",
|
|
18795
18819
|
"machine:directoryListed",
|
|
18796
18820
|
"machine:filePreviewed",
|
|
@@ -5,7 +5,7 @@ import { tickLoops } from "../loop/runner.js";
|
|
|
5
5
|
import { waitForRuntimeSessionRecovery } from "../runtime/session-recovery.js";
|
|
6
6
|
import { handleListDirectory } from "../session/directory.js";
|
|
7
7
|
import { handlePreviewFile } from "../session/file-preview.js";
|
|
8
|
-
import { handleClose, handleCreateSession, handleImportHistoricalSession, handleListHistoricalSessions, handleResume } from "../session/lifecycle.js";
|
|
8
|
+
import { handleClose, handleCreateSession, handleImportHistoricalSession, handleListHistoricalSessions, handleRestoreSession, handleResume } from "../session/lifecycle.js";
|
|
9
9
|
import { handleScheduleAction, handleScheduleCreate, handleScheduleList } from "../loop/handlers.js";
|
|
10
10
|
import { handleMemoryCommand } from "../memory/commands.js";
|
|
11
11
|
import { handleMemoryProjectCommand } from "../memory/project-commands.js";
|
|
@@ -134,6 +134,10 @@ async function handleServerMessage(ws, config, message) {
|
|
|
134
134
|
await handleResume(ws, config, message);
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
+
if (message.type === "machine:restoreSession") {
|
|
138
|
+
await handleRestoreSession(ws, config, message);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
137
141
|
if (message.type === "machine:close") {
|
|
138
142
|
await handleClose(ws, config, message);
|
|
139
143
|
return;
|
|
@@ -70,6 +70,7 @@ export async function registerMachine(ws, config, relayCapabilities) {
|
|
|
70
70
|
daemonInstanceGeneration: instanceGeneration,
|
|
71
71
|
...(lifecycleRegistration ? { daemonLifecycleRegistrationV1: lifecycleRegistration } : {}),
|
|
72
72
|
sessionPrimitives,
|
|
73
|
+
sessionProviderRestoreV1: typeof getRuntime(process.cwd()).restoreSession === "function",
|
|
73
74
|
directoryBrowser: true,
|
|
74
75
|
memory: config.memory?.enabled !== false,
|
|
75
76
|
memoryTurnOptionsV1: true,
|
|
@@ -350,6 +350,13 @@ export function createExternalAgentRuntimeProvider(options) {
|
|
|
350
350
|
}), "ensureSession result handle");
|
|
351
351
|
return toRuntimeHandle(input.sessionKey, handle, input.cwd);
|
|
352
352
|
},
|
|
353
|
+
async restoreSession(input) {
|
|
354
|
+
const client = requireClientForAgent(input.agent);
|
|
355
|
+
if (client || typeof options.fallback.restoreSession !== "function") {
|
|
356
|
+
throw Object.assign(new Error(`Provider ${input.agent} does not support restoring archived Sessions`), { code: "SESSION_RESTORE_UNSUPPORTED", retryable: false });
|
|
357
|
+
}
|
|
358
|
+
await options.fallback.restoreSession(input);
|
|
359
|
+
},
|
|
353
360
|
async attachSession(input) {
|
|
354
361
|
const client = requireClientForAgent(input.agent);
|
|
355
362
|
if (!client) {
|
|
@@ -16,7 +16,9 @@ export type HistoricalBackfillResult = {
|
|
|
16
16
|
export declare function handleCreateSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
17
17
|
export declare function handleImportHistoricalSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
18
18
|
export declare function handleListHistoricalSessions(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
19
|
-
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, operationAlreadyClaimed?: boolean): Promise<SessionState | null>;
|
|
19
|
+
export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, operationAlreadyClaimed?: boolean, failureCode?: string, forceProviderResumeSessionId?: string): Promise<SessionState | null>;
|
|
20
|
+
export declare function refreshRuntimeSessionName(session: SessionState, metadata?: SessionMetadata): Promise<SessionState>;
|
|
21
|
+
export declare function shouldReplaceLocalPlaceholderName(session: SessionState): boolean;
|
|
20
22
|
export declare function usesAuthoritativeTranscript(session: SessionState): boolean;
|
|
21
23
|
export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: DaemonConfig, session: SessionState, runtimeSessionId?: string | undefined, input?: {
|
|
22
24
|
currentHead?: string;
|
|
@@ -28,4 +30,5 @@ export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: Da
|
|
|
28
30
|
strict?: boolean;
|
|
29
31
|
}): Promise<HistoricalBackfillResult>;
|
|
30
32
|
export declare function handleResume(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
33
|
+
export declare function handleRestoreSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
31
34
|
export declare function handleClose(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|