@cydm/happy-elves 0.1.0-beta.79 → 0.1.0-beta.80

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 (156) hide show
  1. package/apps/cli/dist/commands/daemon.js +28 -18
  2. package/apps/cli/dist/commands/gateway.js +22 -3
  3. package/apps/cli/dist/commands/lib/args.js +3 -1
  4. package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +1 -1
  5. package/apps/cli/dist/commands/lib/bootstrap-daemon.js +3 -2
  6. package/apps/cli/dist/commands/lib/bootstrap-device-recovery.js +4 -3
  7. package/apps/cli/dist/commands/lib/bootstrap-output.d.ts +1 -1
  8. package/apps/cli/dist/commands/lib/bootstrap-output.js +1 -1
  9. package/apps/cli/dist/commands/lib/bootstrap.js +7 -4
  10. package/apps/cli/dist/commands/lib/index.d.ts +1 -0
  11. package/apps/cli/dist/commands/lib/index.js +1 -0
  12. package/apps/cli/dist/commands/lib/machine-state.d.ts +3 -0
  13. package/apps/cli/dist/commands/lib/machine-state.js +14 -0
  14. package/apps/cli/dist/commands/lib/orchestrator.d.ts +1 -1
  15. package/apps/cli/dist/commands/lib/orchestrator.js +13 -10
  16. package/apps/cli/dist/commands/lib/session-output.d.ts +8 -1
  17. package/apps/cli/dist/commands/lib/session-output.js +35 -0
  18. package/apps/cli/dist/commands/lib/session-view.d.ts +1 -1
  19. package/apps/cli/dist/commands/lib/session-view.js +42 -44
  20. package/apps/cli/dist/commands/lib/status.js +12 -6
  21. package/apps/cli/dist/commands/lib/types.d.ts +5 -1
  22. package/apps/cli/dist/commands/lib/types.js +1 -1
  23. package/apps/cli/dist/commands/lib/usage.js +14 -9
  24. package/apps/cli/dist/commands/lib/workspace.js +6 -5
  25. package/apps/cli/dist/commands/loop.js +4 -4
  26. package/apps/cli/dist/commands/machine.js +13 -7
  27. package/apps/cli/dist/commands/orchestrator.js +101 -38
  28. package/apps/cli/dist/commands/remote.js +7 -7
  29. package/apps/cli/dist/commands/session.js +52 -28
  30. package/apps/cli/dist/commands/skill.d.ts +3 -1
  31. package/apps/cli/dist/commands/skill.js +44 -15
  32. package/apps/cli/dist/commands/turn.js +98 -6
  33. package/apps/cli/dist/skills/registry.d.ts +1 -1
  34. package/apps/cli/dist/skills/registry.js +13 -2
  35. package/apps/daemon/dist/gateway/runtime.js +279 -126
  36. package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +304 -15
  37. package/apps/daemon/dist/loop/runner.js +398 -163
  38. package/apps/daemon/dist/memory/config.d.ts +2 -2
  39. package/apps/daemon/dist/memory/config.js +5 -4
  40. package/apps/daemon/dist/memory/flush.js +314 -57
  41. package/apps/daemon/dist/paths.d.ts +2 -0
  42. package/apps/daemon/dist/paths.js +2 -0
  43. package/apps/daemon/dist/relay/connection.js +12 -1
  44. package/apps/daemon/dist/relay/register.js +2 -1
  45. package/apps/daemon/dist/relay/send.js +44 -4
  46. package/apps/daemon/dist/runtime/cancel-settlement.js +4 -0
  47. package/apps/daemon/dist/runtime/catalog.d.ts +18 -0
  48. package/apps/daemon/dist/runtime/catalog.js +119 -0
  49. package/apps/daemon/dist/runtime/external-provider.js +59 -30
  50. package/apps/daemon/dist/runtime/provider-start-journal.d.ts +64 -0
  51. package/apps/daemon/dist/runtime/provider-start-journal.js +208 -0
  52. package/apps/daemon/dist/runtime/recovery-context.d.ts +4 -0
  53. package/apps/daemon/dist/runtime/recovery-context.js +28 -0
  54. package/apps/daemon/dist/runtime/session-recovery.d.ts +0 -3
  55. package/apps/daemon/dist/runtime/session-recovery.js +2 -30
  56. package/apps/daemon/dist/runtime/turn-projection.d.ts +27 -0
  57. package/apps/daemon/dist/runtime/turn-projection.js +117 -0
  58. package/apps/daemon/dist/runtime/turn-supervision.d.ts +45 -0
  59. package/apps/daemon/dist/runtime/turn-supervision.js +390 -0
  60. package/apps/daemon/dist/runtime.js +3 -0
  61. package/apps/daemon/dist/session/events.js +19 -0
  62. package/apps/daemon/dist/session/prompt.js +285 -95
  63. package/apps/daemon/dist/session/reconcile.d.ts +4 -0
  64. package/apps/daemon/dist/session/reconcile.js +225 -0
  65. package/apps/daemon/dist/session/retry-recovery.d.ts +3 -0
  66. package/apps/daemon/dist/session/retry-recovery.js +169 -0
  67. package/apps/daemon/dist/state.d.ts +22 -0
  68. package/apps/daemon/dist/turn-coordinator.d.ts +9 -0
  69. package/apps/daemon/dist/turn-coordinator.js +24 -1
  70. package/apps/daemon/dist/types.d.ts +1 -1
  71. package/apps/daemon/package.json +1 -1
  72. package/apps/relay/dist/connections.d.ts +4 -1
  73. package/apps/relay/dist/connections.js +1 -1
  74. package/apps/relay/dist/controller-handlers.d.ts +4 -0
  75. package/apps/relay/dist/controller-handlers.js +399 -9
  76. package/apps/relay/dist/db.js +396 -1
  77. package/apps/relay/dist/event-visibility.d.ts +1 -1
  78. package/apps/relay/dist/event-visibility.js +9 -5
  79. package/apps/relay/dist/http-routes.js +57 -27
  80. package/apps/relay/dist/index.js +3 -1
  81. package/apps/relay/dist/machine-command-result-handlers.js +99 -18
  82. package/apps/relay/dist/machine-handler-context.d.ts +4 -0
  83. package/apps/relay/dist/machine-state.d.ts +27 -0
  84. package/apps/relay/dist/machine-state.js +39 -0
  85. package/apps/relay/dist/projection-repository.js +28 -6
  86. package/apps/relay/dist/projections.js +46 -2
  87. package/apps/relay/dist/relay-context.d.ts +3 -0
  88. package/apps/relay/dist/relay-context.js +186 -17
  89. package/apps/relay/dist/repositories.js +12 -0
  90. package/apps/relay/dist/scope.js +26 -1
  91. package/apps/relay/dist/session-creation-failure.d.ts +4 -0
  92. package/apps/relay/dist/session-creation-failure.js +54 -0
  93. package/apps/relay/dist/session-projection-reducer.js +350 -18
  94. package/apps/relay/dist/turn-repository.d.ts +175 -0
  95. package/apps/relay/dist/turn-repository.js +695 -0
  96. package/apps/relay/dist/types.d.ts +65 -0
  97. package/apps/relay/dist/websocket.js +87 -18
  98. package/build-identity.json +3 -3
  99. package/npm-shrinkwrap.json +5 -5
  100. package/package.json +1 -1
  101. package/packages/agent-sdk/dist/index.d.ts +27 -2
  102. package/packages/agent-sdk/dist/index.js +75 -1
  103. package/packages/client/dist/client.d.ts +7 -2
  104. package/packages/client/dist/client.js +242 -31
  105. package/packages/client/dist/index.d.ts +2 -1
  106. package/packages/client/dist/index.js +1 -0
  107. package/packages/client/dist/live-session-state.js +10 -1
  108. package/packages/client/dist/live-state.js +7 -0
  109. package/packages/client/dist/live-types.d.ts +8 -2
  110. package/packages/client/dist/live.d.ts +4 -0
  111. package/packages/client/dist/live.js +60 -2
  112. package/packages/client/dist/recovery-context.d.ts +37 -0
  113. package/packages/client/dist/recovery-context.js +97 -0
  114. package/packages/client/dist/transport.d.ts +14 -1
  115. package/packages/client/dist/transport.js +85 -52
  116. package/packages/client/dist/turns.d.ts +9 -0
  117. package/packages/client/dist/turns.js +34 -0
  118. package/packages/client/dist/types.d.ts +28 -7
  119. package/packages/client/dist/ui-probe.js +2 -1
  120. package/packages/client/dist/validation.js +30 -12
  121. package/packages/pie-provider/dist/index.js +268 -33
  122. package/packages/pie-provider/dist/turn-observation-store.d.ts +44 -0
  123. package/packages/pie-provider/dist/turn-observation-store.js +306 -0
  124. package/packages/provider-protocol/dist/index.d.ts +520 -1
  125. package/packages/provider-protocol/dist/index.js +123 -2
  126. package/packages/runtime/dist/index.d.ts +35 -3
  127. package/packages/runtime/dist/index.js +66 -1
  128. package/packages/runtime-acpx/dist/index.d.ts +1 -1
  129. package/packages/runtime-acpx/dist/index.js +130 -1
  130. package/packages/runtime-acpx/dist/turn-observation-store.d.ts +40 -0
  131. package/packages/runtime-acpx/dist/turn-observation-store.js +213 -0
  132. package/packages/runtime-cli/dist/claude-turn.d.ts +4 -2
  133. package/packages/runtime-cli/dist/claude-turn.js +54 -20
  134. package/packages/runtime-cli/dist/codex-app-server.d.ts +24 -4
  135. package/packages/runtime-cli/dist/codex-app-server.js +481 -143
  136. package/packages/runtime-cli/dist/codex-protocol.d.ts +1 -1
  137. package/packages/runtime-cli/dist/codex-protocol.js +2 -1
  138. package/packages/runtime-cli/dist/index.js +139 -4
  139. package/packages/runtime-cli/dist/persistent-turn-observation-store.d.ts +50 -0
  140. package/packages/runtime-cli/dist/persistent-turn-observation-store.js +252 -0
  141. package/packages/runtime-cli/dist/turn-observation.d.ts +33 -0
  142. package/packages/runtime-cli/dist/turn-observation.js +114 -0
  143. package/packages/shared/dist/crypto.d.ts +1 -1
  144. package/packages/shared/dist/crypto.js +3 -3
  145. package/packages/shared/dist/index.d.ts +1 -0
  146. package/packages/shared/dist/index.js +1 -0
  147. package/packages/shared/dist/node/gateway-store.d.ts +15 -1
  148. package/packages/shared/dist/node/gateway-store.js +39 -1
  149. package/packages/shared/dist/protocol-schemas.d.ts +87 -4
  150. package/packages/shared/dist/protocol-schemas.js +57 -0
  151. package/packages/shared/dist/protocol-types.d.ts +24 -0
  152. package/packages/shared/dist/protocol.d.ts +141 -2
  153. package/packages/shared/dist/protocol.js +256 -14
  154. package/packages/shared/dist/session-state.js +2 -6
  155. package/packages/shared/dist/turn-state.d.ts +72 -0
  156. package/packages/shared/dist/turn-state.js +24 -0
@@ -2,7 +2,7 @@ import { createHash, timingSafeEqual } from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { daemonLifecycleRequestIdV1Schema, } from "../../../../packages/shared/dist/index.js";
5
- import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, assertNoPendingEnrollmentAuthority, canonicalRelayUrl, cliErrorFromFetch, CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, diagnoseInstallation, emitDoctorResult, ensureDaemonRunning, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readDaemonProjectionBaseline, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, restartDaemonForBootstrap, showMachine, startDaemonTimeoutMs, stopLocalDaemon, stopSpawnedDaemon, throwRelayHttpError, waitForDaemonReadiness, waitForDaemonSelfReadiness, wantsJson, withAccountConfigTransaction } from "./lib/index.js";
5
+ import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, assertNoPendingEnrollmentAuthority, canonicalRelayUrl, cliErrorFromFetch, CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, diagnoseInstallation, emitDoctorResult, ensureDaemonRunning, localDaemonStatus, machineAvailabilityLabel, machineIsAvailable, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readDaemonProjectionBaseline, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, restartDaemonForBootstrap, showMachine, startDaemonTimeoutMs, stopLocalDaemon, stopSpawnedDaemon, throwRelayHttpError, waitForDaemonReadiness, waitForDaemonSelfReadiness, wantsJson, withAccountConfigTransaction } from "./lib/index.js";
6
6
  function positiveLimit(flags, fallback) {
7
7
  const value = flags.limit;
8
8
  if (value === undefined)
@@ -57,7 +57,7 @@ async function waitForLocalDaemonReadiness(client, config, baseline, daemon, req
57
57
  if (readiness.phase === "process-exited" || readiness.phase === "owner-changed") {
58
58
  throw new CliError("Daemon exited before it authenticated with the relay. Run happy-elves daemon doctor and happy-elves daemon logs.", "DAEMON_START_FAILED");
59
59
  }
60
- if (readiness.phase !== "relay-authenticated" || !readiness.machine?.online) {
60
+ if (readiness.phase !== "relay-authenticated" || !machineIsAvailable(readiness.machine)) {
61
61
  const relayDetail = readiness.lastRelayErrorCode ? ` Last relay error: ${readiness.lastRelayErrorCode}.` : "";
62
62
  throw new CliError(`Daemon is running, but it did not authenticate with the relay before the startup deadline.${relayDetail} Run happy-elves daemon doctor and happy-elves daemon logs.`, "DAEMON_ONLINE_TIMEOUT");
63
63
  }
@@ -107,8 +107,8 @@ async function remoteDaemonLifecycle(flags, action) {
107
107
  if (matches.length !== 1) {
108
108
  throw new CliError(`Relay returned duplicate Daemon identities: ${machineId}`, "RELAY_RESPONSE_INVALID");
109
109
  }
110
- if (!machine.online)
111
- throw new CliError(`Daemon is offline: ${machineId}`, "MACHINE_OFFLINE");
110
+ if (!machineIsAvailable(machine))
111
+ throw new CliError(`Daemon is not available: ${machineId}`, "MACHINE_UNAVAILABLE");
112
112
  const capability = machine.capabilities.daemonLifecycleV1;
113
113
  const expectedInstanceGeneration = machine.capabilities.daemonInstanceGeneration;
114
114
  const supported = capability !== undefined &&
@@ -181,9 +181,13 @@ async function daemonOverview(client, input) {
181
181
  const machines = input.machineId ? snapshot.machines.filter((machine) => machine.id === input.machineId) : snapshot.machines;
182
182
  const machineIds = new Set(machines.map((machine) => machine.id));
183
183
  const allSessions = snapshot.sessions.filter((session) => machineIds.has(session.machineId));
184
- const counts = {};
185
- for (const session of allSessions)
186
- counts[session.status] = (counts[session.status] ?? 0) + 1;
184
+ const counts = { supervision: {}, execution: {} };
185
+ for (const session of allSessions) {
186
+ const supervision = session.supervision.status;
187
+ const execution = session.execution.status;
188
+ counts.supervision[supervision] = (counts.supervision[supervision] ?? 0) + 1;
189
+ counts.execution[execution] = (counts.execution[execution] ?? 0) + 1;
190
+ }
187
191
  const sessions = input.includeSessions ? (input.all ? allSessions : allSessions.slice(0, input.limit)) : [];
188
192
  return {
189
193
  machines,
@@ -437,8 +441,9 @@ export async function handleDaemon({ domain, action, flags }) {
437
441
  capabilities: session.capabilities,
438
442
  })) : [];
439
443
  const sessionCount = overview.page.total;
440
- const runningSessionCount = overview.counts.running ?? 0;
441
- const idleSessionCount = idleSessionStatuses.reduce((sum, status) => sum + (overview.counts[status] ?? 0), 0);
444
+ const activeExecutionCount = overview.counts.execution.active ?? 0;
445
+ const unresolvedExecutionCount = (overview.counts.execution.unknown ?? 0) +
446
+ (overview.counts.execution["legacy-unresolved"] ?? 0);
442
447
  const data = {
443
448
  configPath,
444
449
  local,
@@ -448,7 +453,9 @@ export async function handleDaemon({ domain, action, flags }) {
448
453
  machines: includeSessions ? projectedMachines.map((machine) => ({
449
454
  machineId: machine.id,
450
455
  name: machine.metadata.name ?? machine.name,
451
- online: machine.online,
456
+ available: machineIsAvailable(machine),
457
+ daemon: machine.daemon,
458
+ connection: machine.connection,
452
459
  lastSeen: machine.lastSeen,
453
460
  daemonVersion: machine.capabilities.daemonVersion,
454
461
  defaultCwd: machine.defaultCwd,
@@ -459,17 +466,20 @@ export async function handleDaemon({ domain, action, flags }) {
459
466
  })) : machines.map((machine) => ({
460
467
  machineId: machine.id,
461
468
  name: machine.name,
462
- online: machine.online,
469
+ available: machineIsAvailable(machine),
470
+ daemon: machine.daemon,
471
+ connection: machine.connection,
463
472
  lastSeen: machine.lastSeen,
464
473
  })),
465
474
  ...(includeSessions ? { sessions: projectedSessions, page: overview.page } : {}),
466
475
  summary: {
467
476
  machineCount: machines.length,
468
- onlineMachineCount: machines.filter((machine) => machine.online).length,
477
+ availableMachineCount: machines.filter(machineIsAvailable).length,
469
478
  sessionCount,
470
- runningSessionCount,
471
- idleSessionCount,
472
- statusCounts: overview.counts,
479
+ activeExecutionCount,
480
+ unresolvedExecutionCount,
481
+ supervisionCounts: overview.counts.supervision,
482
+ executionCounts: overview.counts.execution,
473
483
  ...(includeSessions ? {
474
484
  blockedSessionCount: projectedSessions.filter((session) => session.orchestration.blocked).length,
475
485
  activeSessionCount: projectedSessions.filter((session) => session.orchestration.state === "active").length,
@@ -481,10 +491,10 @@ export async function handleDaemon({ domain, action, flags }) {
481
491
  if (!wantsJson(flags)) {
482
492
  console.log(`Account: ${data.relayReachable ? "connected" : "unavailable"}`);
483
493
  console.log(data.local.running ? `Local daemon: running${data.local.pid ? ` pid ${data.local.pid}` : ""}` : "Local daemon: stopped");
484
- console.log(`Daemons: ${data.summary.onlineMachineCount}/${data.summary.machineCount} online`);
485
- console.log(`Sessions: ${data.summary.sessionCount} total, ${data.summary.runningSessionCount} running`);
494
+ console.log(`Daemons: ${data.summary.availableMachineCount}/${data.summary.machineCount} available`);
495
+ console.log(`Sessions: ${data.summary.sessionCount} total, ${data.summary.activeExecutionCount} active execution, ${data.summary.unresolvedExecutionCount} unresolved`);
486
496
  for (const machine of data.machines) {
487
- console.log(`- ${machine.name} (${machine.machineId}) ${machine.online ? "online" : "offline"}`);
497
+ console.log(`- ${machine.name} (${machine.machineId}) ${"daemon" in machine && "connection" in machine ? machineAvailabilityLabel(machine) : "unavailable"}`);
488
498
  if ("defaultCwd" in machine)
489
499
  console.log(` cwd: ${machine.defaultCwd ?? "-"}`);
490
500
  }
@@ -30,10 +30,10 @@ export async function handleGateway({ domain, action, positional, flags }) {
30
30
  statePath: gatewayStatePath,
31
31
  });
32
32
  if (!wantsJson(flags)) {
33
- console.log(`Gateway store artifacts: ${inspection.repairRequired ? "repair required" : "healthy"}`);
33
+ console.log(`Gateway store: ${gatewayStoreInspectionLabel(inspection)}`);
34
34
  console.log(`artifact digest: ${inspection.artifactDigest}`);
35
35
  for (const [name, file] of Object.entries(inspection.files)) {
36
- console.log(`${name}: ${file.status}`);
36
+ console.log(`${name}: ${file.status}; contents ${file.contentValidation.status}${file.contentValidation.errorCode ? ` (${file.contentValidation.errorCode})` : ""}`);
37
37
  }
38
38
  return true;
39
39
  }
@@ -681,8 +681,12 @@ async function handleRemoteGateway(action, positional, flags, machineId) {
681
681
  if (action === "store" && positional[0] === "inspect") {
682
682
  const result = await client.inspectGatewayStore(machineId);
683
683
  if (!wantsJson(flags)) {
684
- console.log(`Gateway store on Daemon ${machineId}: ${result.repairRequired ? "repair required" : "healthy"}`);
684
+ console.log(`Gateway store on Daemon ${machineId}: ${gatewayStoreInspectionLabel(result)}`);
685
685
  console.log(`artifact digest: ${result.artifactDigest}`);
686
+ for (const [name, file] of Object.entries(result.files)) {
687
+ const content = file.contentValidation;
688
+ console.log(`${name}: ${file.status}; contents ${content?.status ?? "status unavailable"}${content?.errorCode ? ` (${content.errorCode})` : ""}`);
689
+ }
686
690
  }
687
691
  else
688
692
  ok("gateway.store.inspect", { machineId, ...result }, { machineId });
@@ -1188,6 +1192,21 @@ function stringFlag(flags, key) {
1188
1192
  const value = flags[key];
1189
1193
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
1190
1194
  }
1195
+ function gatewayStoreInspectionLabel(inspection) {
1196
+ if (inspection.repairRequired)
1197
+ return "repair required";
1198
+ const validations = Object.values(inspection.files).map((file) => file.contentValidation?.status);
1199
+ if (validations.some((status) => status === "invalid"))
1200
+ return "content invalid";
1201
+ if (validations.some((status) => status === "unsupported"))
1202
+ return "content version unsupported";
1203
+ if (validations.some((status) => status === "unreadable"))
1204
+ return "content unreadable";
1205
+ // An older Daemon cannot prove the store content it did not inspect.
1206
+ if (validations.some((status) => status === undefined))
1207
+ return "content status unavailable";
1208
+ return "healthy";
1209
+ }
1191
1210
  function isRecord(value) {
1192
1211
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1193
1212
  }
@@ -97,7 +97,7 @@ const valueFlags = new Set([
97
97
  "thought-level",
98
98
  "thinking",
99
99
  "wait-timeout-ms",
100
- "runtime-timeout",
100
+ "inactivity-timeout",
101
101
  "to",
102
102
  "token",
103
103
  "ttl",
@@ -110,6 +110,7 @@ const booleanFlags = new Set([
110
110
  "all",
111
111
  "allow-all-users",
112
112
  "clear-default-chat",
113
+ "confirm-risk",
113
114
  "close-source",
114
115
  "daily",
115
116
  "deny-all-users",
@@ -141,6 +142,7 @@ const booleanFlags = new Set([
141
142
  "respond-all",
142
143
  "summary",
143
144
  "starred",
145
+ "strict",
144
146
  "verbose",
145
147
  "wait",
146
148
  "yes",
@@ -47,7 +47,7 @@ type DaemonReadinessOptions = {
47
47
  * before it is considered ready. This prevents an online snapshot left by
48
48
  * the previous connection from satisfying a new start.
49
49
  */
50
- baseline?: Pick<MachineSnapshot, "online" | "lastSeen">;
50
+ baseline?: Pick<MachineSnapshot, "daemon" | "connection" | "lastSeen">;
51
51
  expectedInstanceGeneration?: string;
52
52
  requireFreshProjection?: boolean;
53
53
  readLocalStatus?: typeof localDaemonStatus;
@@ -4,6 +4,7 @@ import { CliError } from "../../errors.js";
4
4
  import { startDaemonTimeoutMs } from "./paths.js";
5
5
  import { readDaemonConfig } from "./config.js";
6
6
  import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, inspectLocalDaemonRuntime, localDaemonStatus, readLocalDaemonPidIdentity, spawnDaemonStartTracked, stopLocalDaemon, stopSpawnedDaemon, } from "./local-daemon.js";
7
+ import { machineIsAvailable } from "./machine-state.js";
7
8
  const committedConfigMayBeInUseErrors = new WeakSet();
8
9
  export function startFailureMayUseCommittedConfig(error) {
9
10
  return Boolean(error && typeof error === "object" && committedConfigMayBeInUseErrors.has(error));
@@ -331,7 +332,7 @@ export async function waitForDaemonReadiness(client, machineId, options = {}) {
331
332
  continue;
332
333
  }
333
334
  machine = snapshot.machines.find((candidate) => candidate.id === machineId);
334
- if (machine?.online && projectionIsFresh(machine, options)) {
335
+ if (machine && machineIsAvailable(machine) && projectionIsFresh(machine, options)) {
335
336
  // Close the process/projection TOCTOU window: the relay may still expose
336
337
  // a fresh online row for a daemon that exited while snapshot() was in
337
338
  // flight. Re-verify the local owner before declaring startup complete.
@@ -484,7 +485,7 @@ function projectionIsFresh(machine, options) {
484
485
  return true;
485
486
  if (!options.baseline)
486
487
  return true;
487
- if (options.baseline.online !== true)
488
+ if (!machineIsAvailable(options.baseline))
488
489
  return true;
489
490
  return machine.lastSeen > options.baseline.lastSeen;
490
491
  }
@@ -9,6 +9,7 @@ import { CliError } from "../../errors.js";
9
9
  import { ok } from "./json.js";
10
10
  import { assertLocalDaemonVerified, localDaemonStatus, stopSpawnedDaemon, } from "./local-daemon.js";
11
11
  import { withAccountConfigTransaction } from "./config.js";
12
+ import { machineIsAvailable } from "./machine-state.js";
12
13
  import { controllerBaseUrl } from "./bootstrap-config.js";
13
14
  import { readDaemonProjectionBaseline, restartDaemonForBootstrap, waitForDaemonReadiness, } from "./bootstrap-daemon.js";
14
15
  import { openUrl } from "./bootstrap-output.js";
@@ -232,7 +233,7 @@ export async function startDeviceRecoveryBootstrap(params) {
232
233
  const daemonDisplayName = daemon.name && daemon.name !== daemonConfig.machineId
233
234
  ? daemon.name
234
235
  : daemonConfig.machineName;
235
- console.log(`Daemon ${daemonDisplayName} is ${daemon.online ? "online" : "connected; needs attention"}.`);
236
+ console.log(`Daemon ${daemonDisplayName} is ${daemon.online ? "available" : "connected; needs attention"}.`);
236
237
  if (params.flags["no-open"] === true) {
237
238
  console.log("");
238
239
  console.log("Open Happy Elves:");
@@ -301,7 +302,7 @@ async function ensureRecoveredDaemonReady(params) {
301
302
  timeoutMs: Math.min(startDaemonTimeoutMs, 5_000),
302
303
  });
303
304
  return {
304
- online: machine?.online === true,
305
+ online: machineIsAvailable(machine),
305
306
  name: machine?.name ?? params.daemonConfig.machineName,
306
307
  };
307
308
  }
@@ -346,7 +347,7 @@ async function ensureRecoveredDaemonReady(params) {
346
347
  requireFreshProjection: started.started || started.restarted,
347
348
  timeoutMs: startDaemonTimeoutMs,
348
349
  });
349
- if (readiness.phase !== "relay-authenticated" || !readiness.machine?.online) {
350
+ if (readiness.phase !== "relay-authenticated" || !machineIsAvailable(readiness.machine)) {
350
351
  throw new CliError("The Daemon did not authenticate before the startup deadline.", "DAEMON_ONLINE_TIMEOUT");
351
352
  }
352
353
  const confirmedDaemonConfig = await transaction.readDaemon();
@@ -7,7 +7,7 @@ export declare function writeHumanStartOutput(data: {
7
7
  openError?: string;
8
8
  daemon: {
9
9
  name: string;
10
- online: boolean;
10
+ status: string;
11
11
  pid?: number;
12
12
  };
13
13
  }): void;
@@ -18,7 +18,7 @@ export function writeHumanStartOutput(data) {
18
18
  console.log("This one-time link expires in 2 minutes.");
19
19
  console.log("");
20
20
  console.log("Daemon");
21
- console.log(`${data.daemon.online ? "online" : "starting"}, ${data.daemon.name}${data.daemon.pid ? ` (pid ${data.daemon.pid})` : ""}`);
21
+ console.log(`${data.daemon.status}, ${data.daemon.name}${data.daemon.pid ? ` (pid ${data.daemon.pid})` : ""}`);
22
22
  }
23
23
  export function openUrl(url, options = {}) {
24
24
  try {
@@ -14,6 +14,7 @@ import { readDaemonProjectionBaseline, restartDaemonForBootstrap, startFailureMa
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";
17
+ import { machineIsAvailable } from "./machine-state.js";
17
18
  import { clearFreshStartIntent, freshStartIntentMatchesConfigs, loadOrCreateFreshStartIntent, readFreshStartIntent, } from "./fresh-start-intent.js";
18
19
  export async function startBootstrap(flags) {
19
20
  if (typeof flags.join === "string") {
@@ -208,7 +209,7 @@ export async function startBootstrap(flags) {
208
209
  if (readiness.phase === "process-exited" || readiness.phase === "owner-changed") {
209
210
  throw new CliError("Daemon exited before it authenticated with the relay. Run happy-elves doctor and happy-elves daemon logs.", "DAEMON_START_FAILED");
210
211
  }
211
- if (readiness.phase !== "relay-authenticated" || !machine?.online) {
212
+ if (readiness.phase !== "relay-authenticated" || !machineIsAvailable(machine)) {
212
213
  const relayDetail = readiness.lastRelayErrorCode ? ` Last relay error: ${readiness.lastRelayErrorCode}.` : "";
213
214
  throw new CliError(`Daemon is running, but it did not authenticate with the relay before the startup deadline.${relayDetail} Run happy-elves doctor and happy-elves daemon logs.`, "DAEMON_ONLINE_TIMEOUT");
214
215
  }
@@ -307,13 +308,15 @@ export async function startBootstrap(flags) {
307
308
  machine: {
308
309
  id: daemonState.config.machineId,
309
310
  name: machine?.name ?? daemonState.config.machineName,
310
- online: machine?.online ?? false,
311
+ available: machineIsAvailable(machine),
312
+ daemon: machine?.daemon,
313
+ connection: machine?.connection,
311
314
  lastSeen: machine?.lastSeen,
312
315
  },
313
316
  daemon: {
314
317
  status: daemonNeedsAttention
315
318
  ? "needs-attention"
316
- : daemon.local.running ? "online" : "starting",
319
+ : daemon.local.running ? "running" : "starting",
317
320
  pid: daemon.local.pid ?? daemon.startedPid,
318
321
  pidPath: daemon.local.pidPath,
319
322
  started: daemon.started,
@@ -346,7 +349,7 @@ export async function startBootstrap(flags) {
346
349
  openError: openResult.error,
347
350
  daemon: {
348
351
  name: data.machine.name,
349
- online: data.machine.online,
352
+ status: data.machine.available ? "available" : data.machine.connection?.status ?? "starting",
350
353
  pid: data.daemon.pid,
351
354
  },
352
355
  });
@@ -17,6 +17,7 @@ export * from "./doctor.js";
17
17
  export * from "./exit.js";
18
18
  export * from "./json.js";
19
19
  export * from "./local-daemon.js";
20
+ export * from "./machine-state.js";
20
21
  export * from "./orchestrator.js";
21
22
  export * from "./pending-enrollment.js";
22
23
  export * from "./paths.js";
@@ -17,6 +17,7 @@ export * from "./doctor.js";
17
17
  export * from "./exit.js";
18
18
  export * from "./json.js";
19
19
  export * from "./local-daemon.js";
20
+ export * from "./machine-state.js";
20
21
  export * from "./orchestrator.js";
21
22
  export * from "./pending-enrollment.js";
22
23
  export * from "./paths.js";
@@ -0,0 +1,3 @@
1
+ import type { MachineSnapshot } from "../../../../../packages/shared/dist/index.js";
2
+ export declare function machineIsAvailable(machine: Pick<MachineSnapshot, "daemon" | "connection"> | undefined): boolean;
3
+ export declare function machineAvailabilityLabel(machine: Pick<MachineSnapshot, "daemon" | "connection">): string;
@@ -0,0 +1,14 @@
1
+ export function machineIsAvailable(machine) {
2
+ return machine?.daemon.status === "running" && machine.connection.status === "connected";
3
+ }
4
+ export function machineAvailabilityLabel(machine) {
5
+ if (machine.daemon.status === "stopped")
6
+ return "daemon-stopped";
7
+ if (machine.daemon.status === "unknown")
8
+ return "daemon-unknown";
9
+ if (machine.connection.status === "reconnecting")
10
+ return "reconnecting";
11
+ if (machine.connection.status === "disconnected")
12
+ return "disconnected";
13
+ return "available";
14
+ }
@@ -32,7 +32,7 @@ export declare const orchestratorRequestMetadataCapacity = 256;
32
32
  export declare function promptSha256(text: string): string;
33
33
  export declare function deterministicTurnId(threadId: string, requestId: string): string;
34
34
  export declare function deterministicRelayRequestId(threadId: string, requestId: string): string;
35
- export declare function normalizeOrchestratorStatus(session: Pick<SessionSnapshot, "status">, orchestration: OrchestrationSummary, latestTurn?: Pick<TurnSummary, "status">): OrchestratorStatus;
35
+ export declare function normalizeOrchestratorStatus(session: Pick<SessionSnapshot, "status" | "daemon" | "connection" | "supervision" | "execution">, orchestration: OrchestrationSummary, _latestTurn?: Pick<TurnSummary, "status">): OrchestratorStatus;
36
36
  export declare function normalizeOrchestratorTag(value: string): string;
37
37
  export declare function normalizeOrchestratorTags(values: readonly string[]): string[];
38
38
  export declare function readOrchestratorMetadata(metadata: Record<string, unknown>): OrchestratorMetadata;
@@ -16,25 +16,28 @@ export function deterministicRelayRequestId(threadId, requestId) {
16
16
  const digest = createHash("sha256").update("orchestrator").update("\0").update(threadId).update("\0").update(requestId).digest("base64url");
17
17
  return `req_${digest.slice(0, 28)}`;
18
18
  }
19
- export function normalizeOrchestratorStatus(session, orchestration, latestTurn) {
19
+ export function normalizeOrchestratorStatus(session, orchestration, _latestTurn) {
20
20
  if (session.status === "closed")
21
21
  return "closed";
22
- if (session.status === "offline")
23
- return "offline";
24
22
  if (orchestration.needsUser)
25
23
  return "needsInput";
26
- if (orchestration.blocked)
24
+ if (session.supervision.status === "failed" ||
25
+ session.execution.status === "unknown" ||
26
+ session.execution.status === "legacy-unresolved")
27
27
  return "blocked";
28
- if (orchestration.statusConflict?.evidence === "running-turn")
29
- return "inProgress";
30
- if (latestTurn?.status === "running" || session.status === "running" || session.status === "new")
28
+ if (session.execution.status === "active" ||
29
+ session.supervision.status === "watching" ||
30
+ session.supervision.status === "reconciling" ||
31
+ session.supervision.status === "settling")
31
32
  return "inProgress";
32
- if (latestTurn?.status === "failed" || session.status === "failed")
33
+ if (session.execution.status === "failed")
33
34
  return "failed";
34
- if (latestTurn?.status === "cancelled" || session.status === "cancelled")
35
+ if (session.execution.status === "cancelled")
35
36
  return "cancelled";
36
- if (latestTurn?.status === "completed" || orchestration.completed || session.status === "completed")
37
+ if (session.execution.status === "completed")
37
38
  return "completed";
39
+ if (session.daemon.status !== "running" || session.connection.status !== "connected")
40
+ return "offline";
38
41
  return "idle";
39
42
  }
40
43
  export function normalizeOrchestratorTag(value) {
@@ -1,4 +1,5 @@
1
- import type { ControllerClient, DecodedSessionEvent } from "../../../../../packages/client/dist/index.js";
1
+ import { type ControllerClient, type DecodedSessionEvent } from "../../../../../packages/client/dist/index.js";
2
+ import type { TurnSnapshotV2 } from "../../../../../packages/shared/dist/index.js";
2
3
  export declare function compactText(value: string, max?: number): string;
3
4
  export declare function lastAssistantOutput(events: DecodedSessionEvent[]): string;
4
5
  export declare function compareSessionEventLogicalOrder(left: DecodedSessionEvent, right: DecodedSessionEvent): number;
@@ -31,6 +32,12 @@ export type TurnSummary = {
31
32
  lastEventId: number;
32
33
  eventCount: number;
33
34
  };
35
+ /**
36
+ * Keep transcript content for display, but derive settlement exclusively from
37
+ * the immutable turn record. A plain/backfilled `done` event is not proof of
38
+ * provider completion and therefore remains projected as running.
39
+ */
40
+ export declare function applyAuthoritativeTurnSnapshot(summary: TurnSummary | undefined, turn: TurnSnapshotV2 | undefined, requestedTurnId: string): TurnSummary | undefined;
34
41
  export type StructuredCommandOutput = {
35
42
  state: string;
36
43
  sessionId: string;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { authoritativeTerminalTurnStatus, } from "../../../../../packages/client/dist/index.js";
3
4
  import { CliError } from "../../errors.js";
4
5
  import { outputRoot } from "./paths.js";
5
6
  function eventText(event) {
@@ -86,6 +87,40 @@ export function displayPath(filePath) {
86
87
  const relative = path.relative(process.cwd(), filePath);
87
88
  return relative.startsWith("..") ? filePath : relative || ".";
88
89
  }
90
+ /**
91
+ * Keep transcript content for display, but derive settlement exclusively from
92
+ * the immutable turn record. A plain/backfilled `done` event is not proof of
93
+ * provider completion and therefore remains projected as running.
94
+ */
95
+ export function applyAuthoritativeTurnSnapshot(summary, turn, requestedTurnId) {
96
+ if (!summary && !turn)
97
+ return undefined;
98
+ const terminalStatus = authoritativeTerminalTurnStatus(turn);
99
+ if (summary) {
100
+ return {
101
+ ...summary,
102
+ status: terminalStatus ?? "running",
103
+ completedAt: terminalStatus === undefined ? undefined : (summary.completedAt ?? turn?.updatedAt),
104
+ updatedAt: Math.max(summary.updatedAt, turn?.updatedAt ?? 0),
105
+ };
106
+ }
107
+ if (!turn)
108
+ return undefined;
109
+ return {
110
+ turnId: turn.turnId,
111
+ ...(requestedTurnId === turn.turnId ? {} : { runtimeTurnId: requestedTurnId }),
112
+ status: terminalStatus ?? "running",
113
+ prompt: "",
114
+ output: "",
115
+ terminalText: "",
116
+ startedAt: turn.createdAt,
117
+ ...(terminalStatus === undefined ? {} : { completedAt: turn.updatedAt }),
118
+ updatedAt: turn.updatedAt,
119
+ firstEventId: 0,
120
+ lastEventId: 0,
121
+ eventCount: 0,
122
+ };
123
+ }
89
124
  function terminalTextForDone(events, turnId) {
90
125
  const done = [...events].reverse().find((event) => event.turnId === turnId && event.decoded?.type === "done")?.decoded;
91
126
  if (done?.type !== "done")
@@ -1,6 +1,6 @@
1
1
  import type { ControllerClient, DecodedSessionEvent } from "../../../../../packages/client/dist/index.js";
2
2
  import type { MachineSnapshot, SessionSnapshot } from "../../../../../packages/shared/dist/index.js";
3
- import { type MachineMetadata, type OrchestrationSummary, type OrchestrationState } from "./types.js";
3
+ import type { MachineMetadata, OrchestrationSummary, OrchestrationState } from "./types.js";
4
4
  import { type TurnSummary } from "./session-output.js";
5
5
  export declare function showMachine(client: ControllerClient, machine: MachineSnapshot): Promise<MachineSnapshot & {
6
6
  defaultCwd?: string;
@@ -1,6 +1,5 @@
1
1
  import path from "node:path";
2
2
  import { CliError } from "../../errors.js";
3
- import { idleSessionStatuses } from "./types.js";
4
3
  import { summarizeTurns } from "./session-output.js";
5
4
  export async function showMachine(client, machine) {
6
5
  const metadata = (await client.decodeMachineMetadata(machine));
@@ -25,26 +24,42 @@ function eventNeedsUser(event) {
25
24
  return true;
26
25
  return false;
27
26
  }
28
- function orchestrationStateFromStatus(status) {
29
- if (status === "new")
30
- return "pending";
31
- if (status === "running")
27
+ function orchestrationStateFromSnapshot(session) {
28
+ if (session.status === "closed")
29
+ return "closed";
30
+ if (session.supervision.status === "failed")
31
+ return "blocked";
32
+ if (session.execution.status === "unknown" || session.execution.status === "legacy-unresolved")
33
+ return "blocked";
34
+ if (session.execution.status === "failed")
35
+ return "blocked";
36
+ if (session.execution.status === "active" || session.supervision.status === "watching")
37
+ return "active";
38
+ if (session.supervision.status === "reconciling" || session.supervision.status === "settling")
32
39
  return "active";
33
- if (status === "ready" || status === "cancelled")
34
- return "idle";
35
- if (status === "completed")
40
+ if (session.execution.status === "completed")
36
41
  return "completed";
37
- if (status === "failed" || status === "offline")
38
- return "blocked";
39
- return "closed";
42
+ if (session.status === "new" && session.execution.status === "idle")
43
+ return "pending";
44
+ return "idle";
45
+ }
46
+ function canTrustVisibleRunningTurn(session) {
47
+ return session.status !== "closed" &&
48
+ (session.execution.status === "active" || session.supervision.status === "watching");
40
49
  }
41
- function canTrustVisibleRunningTurn(status) {
42
- return status !== "closed" && status !== "offline" && status !== "cancelled";
50
+ function operationalProjection(session) {
51
+ return {
52
+ daemon: session.daemon.status,
53
+ connection: session.connection.status,
54
+ supervision: session.supervision.status,
55
+ execution: session.execution.status,
56
+ rawStatus: session.status,
57
+ };
43
58
  }
44
59
  function visibleRunningTurnSummary(session, turnId) {
45
60
  return {
46
61
  state: "active",
47
- rawStatus: session.status,
62
+ ...operationalProjection(session),
48
63
  needsUser: false,
49
64
  blocked: false,
50
65
  completed: false,
@@ -71,35 +86,16 @@ export function deriveOrchestration(session, events = [], focusTurn) {
71
86
  if (needsUserEvent && latestTerminalTurn && latestTerminalTurn.status !== "completed") {
72
87
  return needsUserSummary(session, needsUserEvent);
73
88
  }
74
- if (latestTurn?.status === "running" && canTrustVisibleRunningTurn(session.status)) {
89
+ if (latestTurn?.status === "running" && canTrustVisibleRunningTurn(session)) {
75
90
  return visibleRunningTurnSummary(session, latestTurn.turnId);
76
91
  }
77
- if (focusTurn?.status === "running" && canTrustVisibleRunningTurn(session.status)) {
92
+ if (focusTurn?.status === "running" && canTrustVisibleRunningTurn(session)) {
78
93
  return visibleRunningTurnSummary(session, focusTurn.turnId);
79
94
  }
80
- if (session.status !== "running") {
81
- const state = orchestrationStateFromStatus(session.status);
82
- return {
83
- state,
84
- rawStatus: session.status,
85
- needsUser: false,
86
- blocked: state === "blocked",
87
- completed: state === "completed",
88
- };
89
- }
90
- if (activeTurn) {
91
- return {
92
- state: "active",
93
- rawStatus: session.status,
94
- needsUser: false,
95
- blocked: false,
96
- completed: false,
97
- };
98
- }
99
- const state = orchestrationStateFromStatus(session.status);
95
+ const state = orchestrationStateFromSnapshot(session);
100
96
  return {
101
97
  state,
102
- rawStatus: session.status,
98
+ ...operationalProjection(session),
103
99
  needsUser: false,
104
100
  blocked: state === "blocked",
105
101
  completed: state === "completed",
@@ -114,7 +110,7 @@ function needsUserSummary(session, event) {
114
110
  : undefined;
115
111
  return {
116
112
  state: "needs-user",
117
- rawStatus: session.status,
113
+ ...operationalProjection(session),
118
114
  needsUser: true,
119
115
  blocked: true,
120
116
  completed: false,
@@ -202,17 +198,19 @@ export async function machineSessionOverview(client, sessions, machineId) {
202
198
  lastTurnId: session.lastTurnId,
203
199
  };
204
200
  }));
201
+ const openSessions = projectedSessions.filter((session) => session.status !== "closed");
202
+ const settledSessions = openSessions.filter((session) => session.orchestration.supervision === "idle");
205
203
  return {
206
204
  summary: {
207
205
  sessionCount: machineSessions.length,
208
- runningSessionCount: projectedSessions.filter((session) => session.status === "running").length,
209
- idleSessionCount: projectedSessions.filter((session) => idleSessionStatuses.includes(session.status)).length,
206
+ runningSessionCount: openSessions.filter((session) => session.orchestration.execution === "active").length,
207
+ idleSessionCount: settledSessions.filter((session) => session.status !== "new" && ["idle", "completed", "cancelled"].includes(session.orchestration.execution)).length,
210
208
  blockedSessionCount: projectedSessions.filter((session) => session.orchestration.blocked).length,
211
- readySessionCount: projectedSessions.filter((session) => session.status === "ready").length,
209
+ readySessionCount: settledSessions.filter((session) => session.status !== "new" && session.orchestration.execution === "idle").length,
212
210
  completedSessionCount: projectedSessions.filter((session) => session.orchestration.completed).length,
213
- failedSessionCount: projectedSessions.filter((session) => session.status === "failed").length,
214
- cancelledSessionCount: projectedSessions.filter((session) => session.status === "cancelled").length,
215
- offlineSessionCount: projectedSessions.filter((session) => session.status === "offline").length,
211
+ failedSessionCount: openSessions.filter((session) => session.orchestration.execution === "failed").length,
212
+ cancelledSessionCount: openSessions.filter((session) => session.orchestration.execution === "cancelled").length,
213
+ offlineSessionCount: openSessions.filter((session) => session.orchestration.daemon !== "running" || session.orchestration.connection !== "connected").length,
216
214
  closedSessionCount: projectedSessions.filter((session) => session.status === "closed").length,
217
215
  activeSessionCount: projectedSessions.filter((session) => session.orchestration.state === "active").length,
218
216
  needsUserSessionCount: projectedSessions.filter((session) => session.orchestration.needsUser).length,