@cydm/happy-elves 0.1.0-beta.74 → 0.1.0-beta.76
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/app.js +2 -0
- package/apps/cli/dist/commands/diagnostics.d.ts +2 -0
- package/apps/cli/dist/commands/diagnostics.js +71 -0
- package/apps/cli/dist/commands/lib/args.js +4 -0
- 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/cli/dist/commands/lib/usage.js +15 -1
- package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +292 -2
- 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/connections.d.ts +1 -0
- package/apps/relay/dist/connections.js +1 -0
- 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.d.ts +1 -0
- package/apps/relay/dist/relay-context.js +21 -14
- package/apps/relay/dist/types.d.ts +8 -1
- package/apps/relay/dist/ui-probe-router.d.ts +35 -0
- package/apps/relay/dist/ui-probe-router.js +414 -0
- package/apps/relay/dist/websocket.js +79 -8
- 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/index.d.ts +3 -1
- package/packages/client/dist/index.js +1 -0
- package/packages/client/dist/transport.d.ts +4 -1
- package/packages/client/dist/types.d.ts +2 -0
- package/packages/client/dist/ui-probe.d.ts +116 -0
- package/packages/client/dist/ui-probe.js +825 -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/index.d.ts +1 -0
- package/packages/shared/dist/index.js +1 -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 +70 -0
- package/packages/shared/dist/protocol.js +84 -0
- package/packages/shared/dist/session-state.d.ts +1 -1
- package/packages/shared/dist/ui-probe.d.ts +459 -0
- package/packages/shared/dist/ui-probe.js +257 -0
|
@@ -2,6 +2,7 @@ import { handleAccount } from "./account.js";
|
|
|
2
2
|
import { handleCollect } from "./collect.js";
|
|
3
3
|
import { handleConfig } from "./config.js";
|
|
4
4
|
import { handleDaemon } from "./daemon.js";
|
|
5
|
+
import { handleDiagnostics } from "./diagnostics.js";
|
|
5
6
|
import { handleGateway } from "./gateway.js";
|
|
6
7
|
import { handleLoop } from "./loop.js";
|
|
7
8
|
import { handleMachine } from "./machine.js";
|
|
@@ -22,6 +23,7 @@ const domainHandlers = [
|
|
|
22
23
|
handleToken,
|
|
23
24
|
handleLoop,
|
|
24
25
|
handleDaemon,
|
|
26
|
+
handleDiagnostics,
|
|
25
27
|
handleRelay,
|
|
26
28
|
handleMachine,
|
|
27
29
|
handleMemory,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { UiProbeClient } from "../../../../packages/client/dist/index.js";
|
|
2
|
+
import { CliError, ok, parseDurationMs, readConfig, requireString, wantsJson } from "./lib/index.js";
|
|
3
|
+
const DEFAULT_CAPTURE_TIMEOUT_MS = 30_000;
|
|
4
|
+
const DEFAULT_WATCH_DURATION_MS = 10 * 60_000;
|
|
5
|
+
const MAX_WATCH_DURATION_MS = 10 * 60_000;
|
|
6
|
+
function durationFlag(flags, key, fallbackMs, maximumMs) {
|
|
7
|
+
const durationMs = parseDurationMs(flags[key], fallbackMs);
|
|
8
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > maximumMs) {
|
|
9
|
+
throw new CliError(`Invalid --${key}: expected a positive duration no greater than 10m`, "INVALID_ARGUMENT");
|
|
10
|
+
}
|
|
11
|
+
return durationMs;
|
|
12
|
+
}
|
|
13
|
+
function printTelemetry(telemetry) {
|
|
14
|
+
console.log(JSON.stringify(telemetry));
|
|
15
|
+
}
|
|
16
|
+
export async function handleDiagnostics({ domain, action, flags }) {
|
|
17
|
+
if (domain !== "diagnostics")
|
|
18
|
+
return false;
|
|
19
|
+
const config = await readConfig(flags);
|
|
20
|
+
const client = new UiProbeClient(config);
|
|
21
|
+
try {
|
|
22
|
+
if (action === "surfaces") {
|
|
23
|
+
const surfaces = await client.listSurfaces();
|
|
24
|
+
if (wantsJson(flags))
|
|
25
|
+
ok("diagnostics.surfaces", { surfaces });
|
|
26
|
+
else if (surfaces.length === 0)
|
|
27
|
+
console.log("No Device is sharing live diagnostics.");
|
|
28
|
+
else {
|
|
29
|
+
for (const surface of surfaces) {
|
|
30
|
+
console.log(`${surface.deviceId} ${surface.surfaceId} ${surface.kind} expires ${new Date(surface.expiresAt).toISOString()}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (action === "capture") {
|
|
36
|
+
const deviceId = requireString(flags, "device");
|
|
37
|
+
const surfaceId = requireString(flags, "surface");
|
|
38
|
+
const timeoutMs = durationFlag(flags, "timeout", DEFAULT_CAPTURE_TIMEOUT_MS, MAX_WATCH_DURATION_MS);
|
|
39
|
+
const telemetry = await client.capture({ deviceId, surfaceId, timeoutMs });
|
|
40
|
+
if (wantsJson(flags))
|
|
41
|
+
ok("diagnostics.capture", telemetry);
|
|
42
|
+
else
|
|
43
|
+
console.log(JSON.stringify(telemetry, null, 2));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (action === "watch") {
|
|
47
|
+
if (flags.jsonl !== true) {
|
|
48
|
+
throw new CliError("diagnostics watch requires --jsonl", "MISSING_ARGUMENT");
|
|
49
|
+
}
|
|
50
|
+
const deviceId = requireString(flags, "device");
|
|
51
|
+
const surfaceId = requireString(flags, "surface");
|
|
52
|
+
const durationMs = durationFlag(flags, "duration", DEFAULT_WATCH_DURATION_MS, MAX_WATCH_DURATION_MS);
|
|
53
|
+
const abortController = new AbortController();
|
|
54
|
+
const onSigint = () => abortController.abort();
|
|
55
|
+
process.once("SIGINT", onSigint);
|
|
56
|
+
try {
|
|
57
|
+
const watch = await client.watch({ deviceId, surfaceId, durationMs, signal: abortController.signal });
|
|
58
|
+
for await (const telemetry of watch)
|
|
59
|
+
printTelemetry(telemetry);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
process.removeListener("SIGINT", onSigint);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
throw new CliError(`Unknown diagnostics action: ${action ?? "(missing)"}`, "INVALID_ARGUMENT");
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await client.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -25,7 +25,9 @@ const valueFlags = new Set([
|
|
|
25
25
|
"device-id",
|
|
26
26
|
"device-name",
|
|
27
27
|
"display-name",
|
|
28
|
+
"device",
|
|
28
29
|
"domain",
|
|
30
|
+
"duration",
|
|
29
31
|
"from",
|
|
30
32
|
"host",
|
|
31
33
|
"host-machine",
|
|
@@ -69,6 +71,7 @@ const valueFlags = new Set([
|
|
|
69
71
|
"runtime-limit",
|
|
70
72
|
"runtime-session",
|
|
71
73
|
"shell",
|
|
74
|
+
"surface",
|
|
72
75
|
"actions",
|
|
73
76
|
"expires-in",
|
|
74
77
|
"format",
|
|
@@ -122,6 +125,7 @@ const booleanFlags = new Set([
|
|
|
122
125
|
"include-archived",
|
|
123
126
|
"isolated",
|
|
124
127
|
"json",
|
|
128
|
+
"jsonl",
|
|
125
129
|
"keep-source",
|
|
126
130
|
"local",
|
|
127
131
|
"longterm",
|
|
@@ -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 ? {
|
|
@@ -12,6 +12,15 @@ Show the current Account, Daemon, and latest Session state.
|
|
|
12
12
|
doctor: `Usage: happy-elves doctor [--relay <url>] [--json]
|
|
13
13
|
|
|
14
14
|
Check local Device access, Account consistency, service reachability, and Daemon state.
|
|
15
|
+
`,
|
|
16
|
+
diagnostics: `Usage:
|
|
17
|
+
happy-elves diagnostics surfaces --json
|
|
18
|
+
happy-elves diagnostics capture --device <deviceId> --surface <surfaceId> [--timeout 30s] --json
|
|
19
|
+
happy-elves diagnostics watch --device <deviceId> --surface <surfaceId> [--duration 10m] --jsonl
|
|
20
|
+
|
|
21
|
+
Observe privacy-filtered layout, focus, viewport, pointer, and scroll telemetry
|
|
22
|
+
from a Device that explicitly enabled Settings > Diagnostics > Share live diagnostics.
|
|
23
|
+
Probe traffic is end-to-end encrypted and expires after ten minutes.
|
|
15
24
|
`,
|
|
16
25
|
account: `Usage:
|
|
17
26
|
happy-elves account create --relay <url> [--device-id <id>] [--device-name <name>] [--reveal-secrets] --json
|
|
@@ -300,6 +309,11 @@ Advanced and compatibility commands:
|
|
|
300
309
|
relay status [--relay <url>] [--json]
|
|
301
310
|
relay doctor [--relay <url>] [--json]
|
|
302
311
|
|
|
312
|
+
# Privacy-filtered diagnostics shared by another trusted Device.
|
|
313
|
+
diagnostics surfaces --json
|
|
314
|
+
diagnostics capture --device <deviceId> --surface <surfaceId> --json
|
|
315
|
+
diagnostics watch --device <deviceId> --surface <surfaceId> --jsonl
|
|
316
|
+
|
|
303
317
|
Usage:
|
|
304
318
|
happy-elves <domain> <action> [args] [flags]
|
|
305
319
|
happy-elves <domain> --help
|
|
@@ -316,7 +330,7 @@ More:
|
|
|
316
330
|
happy-elves --version
|
|
317
331
|
happy-elves version --json
|
|
318
332
|
happy-elves <domain> --help
|
|
319
|
-
start, status, doctor, collect, config, account, daemon, relay, token, gateway, loop.
|
|
333
|
+
start, status, doctor, collect, config, account, daemon, relay, diagnostics, token, gateway, loop.
|
|
320
334
|
orchestrator, skill.
|
|
321
335
|
`;
|
|
322
336
|
}
|