@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.
Files changed (44) hide show
  1. package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +22 -1
  2. package/apps/cli/dist/commands/lib/bootstrap-daemon.js +73 -1
  3. package/apps/cli/dist/commands/lib/bootstrap-device-recovery.d.ts +5 -2
  4. package/apps/cli/dist/commands/lib/bootstrap-device-recovery.js +42 -51
  5. package/apps/cli/dist/commands/lib/bootstrap.js +14 -13
  6. package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +24 -0
  7. package/apps/daemon/dist/relay/connection.js +5 -1
  8. package/apps/daemon/dist/relay/register.js +1 -0
  9. package/apps/daemon/dist/relay/send.js +1 -0
  10. package/apps/daemon/dist/runtime/external-provider.js +7 -0
  11. package/apps/daemon/dist/session/lifecycle.d.ts +4 -1
  12. package/apps/daemon/dist/session/lifecycle.js +156 -20
  13. package/apps/daemon/dist/session/metadata.d.ts +2 -1
  14. package/apps/daemon/dist/session/metadata.js +10 -1
  15. package/apps/daemon/dist/session/prompt.js +58 -7
  16. package/apps/daemon/dist/turn-coordinator.d.ts +1 -1
  17. package/apps/daemon/dist/types.d.ts +2 -0
  18. package/apps/daemon/package.json +1 -1
  19. package/apps/relay/dist/controller-handlers.js +91 -4
  20. package/apps/relay/dist/db.js +2 -0
  21. package/apps/relay/dist/machine-command-result-handlers.js +59 -4
  22. package/apps/relay/dist/machine-handler-context.d.ts +7 -0
  23. package/apps/relay/dist/projections.js +5 -0
  24. package/apps/relay/dist/relay-context.js +7 -2
  25. package/apps/relay/dist/types.d.ts +1 -0
  26. package/apps/relay/dist/websocket.js +1 -0
  27. package/build-identity.json +2 -2
  28. package/npm-shrinkwrap.json +5 -5
  29. package/package.json +1 -1
  30. package/packages/client/dist/client.d.ts +1 -0
  31. package/packages/client/dist/client.js +9 -0
  32. package/packages/client/dist/types.d.ts +2 -0
  33. package/packages/runtime/dist/index.d.ts +14 -0
  34. package/packages/runtime-cli/dist/codex-app-server.d.ts +5 -0
  35. package/packages/runtime-cli/dist/codex-app-server.js +126 -0
  36. package/packages/runtime-cli/dist/index.js +8 -0
  37. package/packages/runtime-cli/dist/session-store.d.ts +1 -0
  38. package/packages/runtime-cli/dist/session-store.js +32 -0
  39. package/packages/shared/dist/protocol-schemas.d.ts +5 -0
  40. package/packages/shared/dist/protocol-schemas.js +4 -0
  41. package/packages/shared/dist/protocol-types.d.ts +4 -0
  42. package/packages/shared/dist/protocol.d.ts +16 -0
  43. package/packages/shared/dist/protocol.js +20 -0
  44. 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>>, machineName: string, readinessError: unknown, options?: {
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, ensureRunningDaemonAutostart, inspectLocalDaemonRuntime, localDaemonStatus, stopSpawnedDaemon, } from "./local-daemon.js";
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 { ensureDaemonRunning, readDaemonProjectionBaseline, waitForDaemonReadiness, } from "./bootstrap-daemon.js";
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
- started = await ensureDaemonRunning(params.daemonConfig, params.cwd);
361
- }
362
- catch (error) {
363
- const projection = await readProjection();
364
- return {
365
- ...projection,
366
- ...(current.pid ? { pid: current.pid } : {}),
367
- started: false,
368
- needsAttention: error instanceof Error ? error.message : "The Daemon could not be started automatically.",
369
- };
370
- }
371
- try {
372
- const readiness = await waitForDaemonReadiness(client, params.daemonConfig.machineId, {
373
- baseline,
374
- expectedInstanceGeneration: started.instanceGeneration,
375
- requireFreshProjection: started.started,
376
- timeoutMs: startDaemonTimeoutMs,
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
- if (readiness.phase !== "relay-authenticated" || !readiness.machine?.online) {
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
- name: readiness.machine.name ?? params.daemonConfig.machineName,
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
- return await projectRecoveredDaemonReadinessFailure(started, params.daemonConfig.machineName, error);
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 { ensureDaemonRunning, readDaemonProjectionBaseline, startFailureMayUseCommittedConfig, waitForDaemonReadiness, } from "./bootstrap-daemon.js";
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
- // A running owner is accepted only when its signed runtime marker proves
89
- // that it loaded the same daemon identity we just read under this lease.
90
- // Ordinary `start` never restarts a healthy daemon because package files
91
- // happen to have a newer mtime.
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 = runtime.state === "missing"
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 ensureDaemonRunning(daemonState.config, cwd);
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: daemonBefore.running || daemon.committedConfigMayBeInUse === true,
321
- restarted: false,
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,
@@ -14,6 +14,7 @@ const criticalMessageTypes = new Set([
14
14
  "machine:sessionHeadAdvanced",
15
15
  "machine:turnDone",
16
16
  "machine:sessionResumed",
17
+ "machine:sessionRestored",
17
18
  "machine:sessionRewound",
18
19
  "machine:sessionForked",
19
20
  "machine:sessionRenamed",
@@ -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>;