@crouton-kit/crouter 0.3.39 → 0.3.41

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 (188) hide show
  1. package/dist/build-root.js +1 -0
  2. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
  6. package/dist/clients/attach/attach-cmd.js +706 -733
  7. package/dist/clients/attach/chat-view.js +11 -0
  8. package/dist/clients/web/server.js +5 -28
  9. package/dist/clients/web/web-cmd.js +1 -1
  10. package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
  11. package/dist/commands/canvas-rebuild-index.js +5 -5
  12. package/dist/commands/node-snapshot.js +9 -1
  13. package/dist/commands/node.js +37 -17
  14. package/dist/commands/push.js +8 -0
  15. package/dist/commands/revive.d.ts +10 -0
  16. package/dist/commands/revive.js +28 -10
  17. package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
  18. package/dist/commands/sys/doctor.js +1 -1
  19. package/dist/commands/sys/setup-core.js +3 -2
  20. package/dist/commands/sys/setup.js +1 -1
  21. package/dist/commands/worktree.d.ts +2 -0
  22. package/dist/commands/worktree.js +94 -0
  23. package/dist/core/__tests__/boot.test.js +4 -4
  24. package/dist/core/__tests__/canvas.test.js +19 -7
  25. package/dist/core/__tests__/child-followup.test.js +15 -5
  26. package/dist/core/__tests__/daemon-boot.test.js +6 -1
  27. package/dist/core/__tests__/daemon-wedge.test.js +18 -1
  28. package/dist/core/__tests__/fault-classifier.test.js +30 -0
  29. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
  30. package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
  31. package/dist/core/__tests__/full/spike-harness.test.js +8 -5
  32. package/dist/core/__tests__/grace-clock.test.js +18 -2
  33. package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
  34. package/dist/core/__tests__/prune-to-limit.test.js +14 -0
  35. package/dist/core/__tests__/review-model-floor.test.js +32 -0
  36. package/dist/core/__tests__/revive.test.js +18 -4
  37. package/dist/core/__tests__/session-cycles.test.js +77 -0
  38. package/dist/core/__tests__/worktree.test.js +85 -0
  39. package/dist/core/canvas/boot.js +12 -7
  40. package/dist/core/canvas/canvas.d.ts +27 -8
  41. package/dist/core/canvas/canvas.js +54 -26
  42. package/dist/core/canvas/db.js +14 -0
  43. package/dist/core/canvas/history.js +1 -0
  44. package/dist/core/canvas/paths.d.ts +10 -9
  45. package/dist/core/canvas/paths.js +10 -9
  46. package/dist/core/canvas/pid.d.ts +155 -1
  47. package/dist/core/canvas/pid.js +306 -1
  48. package/dist/core/canvas/status-glyph.d.ts +7 -0
  49. package/dist/core/canvas/status-glyph.js +10 -1
  50. package/dist/core/canvas/types.d.ts +34 -0
  51. package/dist/core/fault-classifier.js +5 -1
  52. package/dist/core/runtime/bearings.js +4 -0
  53. package/dist/core/runtime/branded-host.d.ts +7 -0
  54. package/dist/core/runtime/branded-host.js +44 -17
  55. package/dist/core/runtime/broker-sdk.js +28 -68
  56. package/dist/core/runtime/broker.js +35 -4
  57. package/dist/core/runtime/host.d.ts +3 -3
  58. package/dist/core/runtime/host.js +148 -33
  59. package/dist/core/runtime/launch.d.ts +15 -15
  60. package/dist/core/runtime/launch.js +58 -4
  61. package/dist/core/runtime/naming.js +3 -2
  62. package/dist/core/runtime/nodes.d.ts +3 -1
  63. package/dist/core/runtime/nodes.js +1 -0
  64. package/dist/core/runtime/pi-cli.d.ts +2 -0
  65. package/dist/core/runtime/pi-cli.js +51 -0
  66. package/dist/core/runtime/placement.d.ts +1 -1
  67. package/dist/core/runtime/placement.js +5 -2
  68. package/dist/core/runtime/recap.js +2 -1
  69. package/dist/core/runtime/revive.d.ts +21 -15
  70. package/dist/core/runtime/revive.js +87 -52
  71. package/dist/core/runtime/session-cycles.d.ts +30 -0
  72. package/dist/core/runtime/session-cycles.js +77 -0
  73. package/dist/core/runtime/spawn.d.ts +4 -0
  74. package/dist/core/runtime/spawn.js +152 -100
  75. package/dist/core/runtime/tmux.d.ts +7 -10
  76. package/dist/core/runtime/tmux.js +9 -11
  77. package/dist/core/worktree.d.ts +35 -0
  78. package/dist/core/worktree.js +158 -0
  79. package/dist/daemon/crtrd.d.ts +17 -8
  80. package/dist/daemon/crtrd.js +191 -40
  81. package/dist/index.d.ts +3 -0
  82. package/dist/index.js +7 -0
  83. package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
  84. package/dist/pi-extensions/canvas-context-intro.js +34 -23
  85. package/dist/pi-extensions/canvas-stophook.js +11 -5
  86. package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
  87. package/dist/web-client/assets/index-DrkcvANq.css +2 -0
  88. package/dist/web-client/index.html +2 -2
  89. package/docs/compat/hearth-crtr-v1.md +191 -0
  90. package/docs/public-api.md +75 -0
  91. package/package.json +3 -4
  92. package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
  93. package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
  94. package/dist/core/hearth/config.d.ts +0 -3
  95. package/dist/core/hearth/config.js +0 -108
  96. package/dist/core/hearth/guest-env.d.ts +0 -9
  97. package/dist/core/hearth/guest-env.js +0 -27
  98. package/dist/core/hearth/index.d.ts +0 -4
  99. package/dist/core/hearth/index.js +0 -4
  100. package/dist/core/hearth/model-auth-guest.d.ts +0 -8
  101. package/dist/core/hearth/model-auth-guest.js +0 -430
  102. package/dist/core/hearth/provider.d.ts +0 -36
  103. package/dist/core/hearth/provider.js +0 -10
  104. package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -254
  105. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
  106. package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
  107. package/dist/core/hearth/providers/blaxel-home.d.ts +0 -54
  108. package/dist/core/hearth/providers/blaxel-home.js +0 -386
  109. package/dist/core/hearth/providers/blaxel.d.ts +0 -36
  110. package/dist/core/hearth/providers/blaxel.js +0 -364
  111. package/dist/core/hearth/providers/types.d.ts +0 -93
  112. package/dist/core/hearth/types.d.ts +0 -155
  113. package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
  114. package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
  115. package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
  116. package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
  117. package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
  118. package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
  119. package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
  120. package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
  121. package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
  122. package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
  123. package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
  124. package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
  125. package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
  126. package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
  127. package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
  128. package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
  129. package/dist/hearth/control-plane/config.d.ts +0 -21
  130. package/dist/hearth/control-plane/config.js +0 -77
  131. package/dist/hearth/control-plane/db.d.ts +0 -30
  132. package/dist/hearth/control-plane/db.js +0 -561
  133. package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
  134. package/dist/hearth/control-plane/hearth-target.js +0 -68
  135. package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
  136. package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
  137. package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
  138. package/dist/hearth/control-plane/ingress/route-store.js +0 -61
  139. package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
  140. package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
  141. package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
  142. package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
  143. package/dist/hearth/control-plane/main.d.ts +0 -1
  144. package/dist/hearth/control-plane/main.js +0 -88
  145. package/dist/hearth/control-plane/node-message.d.ts +0 -31
  146. package/dist/hearth/control-plane/node-message.js +0 -98
  147. package/dist/hearth/control-plane/register.d.ts +0 -15
  148. package/dist/hearth/control-plane/register.js +0 -34
  149. package/dist/hearth/control-plane/registry.d.ts +0 -22
  150. package/dist/hearth/control-plane/registry.js +0 -168
  151. package/dist/hearth/control-plane/relay.d.ts +0 -44
  152. package/dist/hearth/control-plane/relay.js +0 -711
  153. package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
  154. package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
  155. package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
  156. package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
  157. package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
  158. package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
  159. package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
  160. package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
  161. package/dist/hearth/control-plane/secrets.d.ts +0 -31
  162. package/dist/hearth/control-plane/secrets.js +0 -134
  163. package/dist/hearth/control-plane/server.d.ts +0 -27
  164. package/dist/hearth/control-plane/server.js +0 -482
  165. package/dist/hearth/control-plane/serving.d.ts +0 -15
  166. package/dist/hearth/control-plane/serving.js +0 -106
  167. package/dist/hearth/control-plane/session.d.ts +0 -68
  168. package/dist/hearth/control-plane/session.js +0 -273
  169. package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
  170. package/dist/hearth/control-plane/triggers/acl.js +0 -52
  171. package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
  172. package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
  173. package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
  174. package/dist/hearth/control-plane/triggers/deliver.js +0 -76
  175. package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
  176. package/dist/hearth/control-plane/triggers/envelope.js +0 -38
  177. package/dist/hearth/control-plane/types.d.ts +0 -86
  178. package/dist/hearth/control-plane/types.js +0 -1
  179. package/dist/hearth/control-plane/wake.d.ts +0 -86
  180. package/dist/hearth/control-plane/wake.js +0 -550
  181. package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
  182. /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
  183. /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
  184. /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
  185. /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
  186. /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
  187. /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
  188. /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
@@ -1,386 +0,0 @@
1
- import { setTimeout as delay } from 'node:timers/promises';
2
- import { general } from '../../errors.js';
3
- import { nowIso } from '../../fs-utils.js';
4
- import { guestEnv } from '../guest-env.js';
5
- import { buildBootstrapScript } from './blaxel-bootstrap.js';
6
- import { BlaxelMachineProvider } from './blaxel.js';
7
- // recreate-race-fix.md §3.2 — a volume GET is one cheap API call; 1s resolution is plenty
8
- // against an expected seconds-scale detach.
9
- const VOLUME_DETACH_POLL_MS = 1_000;
10
- // Expected wait is seconds (observed live); 120s is ~20-60x headroom over that, so this still
11
- // fails deterministically inside one wake's lifetime rather than hanging forever on a genuine
12
- // platform-side stuck detach.
13
- const VOLUME_DETACH_TIMEOUT_MS = 120_000;
14
- // orphan-cleanup-fix.md §4 — pause before the single bounded retry of a create that failed on a
15
- // server-side deployment transient, to let the platform-side condition clear.
16
- const CREATE_RETRY_DELAY_MS = 5_000;
17
- function stableRouteName(sandboxId) {
18
- return `${sandboxId}-web`;
19
- }
20
- /** Mint a fresh per-generation sandbox name. `providerVolumeId` is the home's stable base
21
- * (never suffixed), so names stay bounded: `hearth-zt-test-1-gmchq3x2a`. Sequential recreates
22
- * are seconds apart, so a ms timestamp cannot collide for one home (recreate-race-fix.md §3.2). */
23
- function mintSandboxName(providerVolumeId) {
24
- return `${providerVolumeId}-g${Date.now().toString(36)}`;
25
- }
26
- /** Escape a volumeId for safe use inside a RegExp so the generation-family anchor is exact —
27
- * a home base name is user-influenced, so a literal `.` or `+` in it must not become a
28
- * wildcard (orphan-cleanup-fix.md §3.1). */
29
- function escapeRegExp(value) {
30
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
- }
32
- /** Predicate matching every generation of a home's name family: the stable base itself, or a
33
- * `<base>-g<suffix>` generation. The `-g` anchor is load-bearing — it prevents `<base>0-g…`
34
- * (e.g. `hearth-zt-test-10-g…`) from matching base `hearth-zt-test-1` (orphan-cleanup-fix.md
35
- * §3.1). Suffix chars are base36 (lowercase alnum), matching `mintSandboxName`. */
36
- function generationFamilyMatcher(volumeId) {
37
- const gen = new RegExp(`^${escapeRegExp(volumeId)}-g[0-9a-z]+$`);
38
- return (name) => name === volumeId || gen.test(name);
39
- }
40
- /** Serialize an arbitrary rejection to a non-empty marker string. `JSON.stringify` RETURNS
41
- * `undefined` (does not throw) for `undefined`, a function, or a symbol — falling through to
42
- * that would omit the `error` field from the marker's JSON line entirely, so fall back to
43
- * `String(...)` whenever it yields `undefined` (matches wake.ts/relay.ts `serializeError`). */
44
- function serialize(error) {
45
- if (error instanceof Error)
46
- return error.message;
47
- try {
48
- const json = JSON.stringify(error);
49
- return json === undefined ? String(error) : json;
50
- }
51
- catch {
52
- return String(error);
53
- }
54
- }
55
- /** Emit one structured stderr line for a recreate-lifecycle marker (sweep / volume_wait /
56
- * cleanup_failed / create_retry). Single writer for all four so the JSON shape stays uniform
57
- * and ops log-alerting keys on `marker` (orphan-cleanup-fix.md §3.3/§3.4/§4). */
58
- function emitMarker(marker, fields, level = 'info') {
59
- process.stderr.write(`${JSON.stringify({ level, marker, ...fields })}\n`);
60
- }
61
- /** Classify a failed create+deploy as a retryable server-side deployment transient
62
- * (orphan-cleanup-fix.md §4). Retryable IFF `status_code >= 500` OR `code === 'DEPLOYMENT_FAILED'`
63
- * on the rejection object, with a message-substring fallback for Error-wrapped variants whose
64
- * structured fields were flattened into `.message`. NEVER on 4xx — a 409 here is a real name
65
- * conflict the design must surface, not absorb, so an explicit 4xx status vetoes the retry even
66
- * if a `DEPLOYMENT_FAILED` code is also present. */
67
- function isRetryableDeploymentError(error) {
68
- if (error !== null && typeof error === 'object') {
69
- // Only the canonical HTTP-status keys — NOT a bare `status`, which is an overloaded field
70
- // name some envelopes use for a non-HTTP state enum/count (MINOR-2).
71
- const obj = error;
72
- const status = [obj.status_code, obj.statusCode].find((v) => typeof v === 'number');
73
- if (typeof status === 'number') {
74
- if (status >= 400 && status < 500)
75
- return false; // 4xx: real conflict, never absorb
76
- if (status >= 500)
77
- return true;
78
- }
79
- if (obj.code === 'DEPLOYMENT_FAILED')
80
- return true;
81
- }
82
- // Fallback for Error-wrapped variants: only POSITIVE server-side signals, never inferring a 4xx.
83
- const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
84
- if (message !== '') {
85
- if (/DEPLOYMENT_FAILED/.test(message))
86
- return true;
87
- if (/\bstatus[_ ]?code\b\D{0,8}5\d\d/i.test(message))
88
- return true;
89
- }
90
- return false;
91
- }
92
- function ensureReadyHome(input) {
93
- if (input.providerName !== 'blaxel') {
94
- throw general(`unsupported Hearth home provider ${input.providerName}`, { providerName: input.providerName });
95
- }
96
- if (typeof input.providerSandboxId !== 'string' || input.providerSandboxId === '') {
97
- throw general(`home ${input.homeId} is missing providerSandboxId`, { homeId: input.homeId });
98
- }
99
- if (typeof input.providerVolumeId !== 'string' || input.providerVolumeId === '') {
100
- throw general(`home ${input.homeId} is missing providerVolumeId`, { homeId: input.homeId });
101
- }
102
- if (typeof input.homeAgentTarget !== 'string' || input.homeAgentTarget === '') {
103
- throw general(`home ${input.homeId} is missing homeAgentTarget`, { homeId: input.homeId });
104
- }
105
- if (typeof input.relayToken !== 'string' || input.relayToken === '') {
106
- throw general(`home ${input.homeId} is missing resolved relayToken`, { homeId: input.homeId });
107
- }
108
- return input;
109
- }
110
- function isRunningMachine(machine) {
111
- return machine !== null && machine.state !== 'terminated';
112
- }
113
- function previewEndpoint(route, fallback) {
114
- return route.concreteUrl ?? route.url ?? fallback ?? null;
115
- }
116
- function refreshedPointers(home, provider, route, status, sandboxId, machine) {
117
- return {
118
- providerName: 'blaxel',
119
- providerSandboxId: sandboxId,
120
- providerVolumeId: home.providerVolumeId,
121
- previewEndpoint: route === null ? (home.previewEndpoint ?? null) : previewEndpoint(route, home.previewEndpoint),
122
- route,
123
- homeAgentTarget: home.homeAgentTarget,
124
- relayAuthRef: home.relayAuthRef ?? null,
125
- templateVersion: provider.templateVersion,
126
- status,
127
- machine,
128
- updatedAt: nowIso(),
129
- };
130
- }
131
- export class BlaxelHomeBackend {
132
- config;
133
- provider;
134
- constructor(config, provider) {
135
- this.config = config;
136
- this.provider = provider ?? new BlaxelMachineProvider({ image: config.imageRef, templateVersion: config.templateVersion, volumeSizeGb: config.volumeSizeGb });
137
- }
138
- async wake(homeInput) {
139
- const current = ensureReadyHome(homeInput);
140
- const machine = await this.getMachine(current.providerSandboxId);
141
- const bootstrapScript = buildBootstrapScript({
142
- guestHome: this.config.guestHome,
143
- crtrVersion: this.provider.templateVersion,
144
- relayToken: current.relayToken,
145
- webPort: this.config.webPort,
146
- nodeId: current.homeAgentTarget,
147
- piAuthJson: this.config.piAuthJson,
148
- piPackages: this.config.piPackages,
149
- isFirstProvision: false,
150
- });
151
- if (isRunningMachine(machine)) {
152
- // Standby resume (R-SUSP-2/R-WAKE-3): the sandbox already exists — never call
153
- // createMachine() here. Probing the port is enough to make Blaxel transparently restore
154
- // a snapshot-paused sandbox; the broker pid inside the guest is untouched either way, and
155
- // nothing on this host-side path invokes reviveNode() (that launcher is for crtr's own
156
- // node engine, not a guest process — the provider layer never imports it). The exec
157
- // fallback below only re-runs the idempotent in-guest bootstrap when the port still isn't
158
- // answering after a snapshot-resume window; it is not a recreate.
159
- try {
160
- await this.provider.waitForPort(current.providerSandboxId, this.config.webPort, { maxWaitMs: 30_000 });
161
- }
162
- catch {
163
- await this.provider.exec(current.providerSandboxId, { command: bootstrapScript, background: true, cwd: this.config.guestHome });
164
- await this.provider.waitForPort(current.providerSandboxId, this.config.webPort, { maxWaitMs: 30_000 });
165
- }
166
- const route = await this.provider.publishHttpPort(current.providerSandboxId, {
167
- machineId: current.providerSandboxId,
168
- targetPort: this.config.webPort,
169
- routeName: stableRouteName(current.providerSandboxId),
170
- routeId: stableRouteName(current.providerSandboxId),
171
- });
172
- return refreshedPointers(current, this.provider, route, 'running', current.providerSandboxId, await this.getMachine(current.providerSandboxId));
173
- }
174
- // Terminated-machine branch (recreate-race-fix.md §3.4, orphan-cleanup-fix.md §3.3/§3.4):
175
- // the crash-recovery path for a sandbox deleted recently out-of-band OR by a crashed/failed
176
- // roll (a Fly redeploy restarting the CP mid-roll lands here on the next wake).
177
- // `sweepAndReleaseVolume` destroys EVERY generation in this home's name family — including
178
- // an unpointed leftover a crashed roll left holding the volume, which the old point-in-time
179
- // attachedTo read could miss once Blaxel autonomously detached it on idle→STANDBY — then
180
- // polls until the volume is confirmed free before minting a fresh name and creating.
181
- await this.sweepAndReleaseVolume(current.providerVolumeId, current.homeId);
182
- const { sandboxName, route, machine: created } = await this.createGeneration({
183
- homeId: current.homeId,
184
- tenantId: current.tenantId,
185
- volumeId: current.providerVolumeId,
186
- bootstrapScript,
187
- });
188
- return refreshedPointers(current, this.provider, route, 'running', sandboxName, created);
189
- }
190
- /** Destroy EVERY generation of this home via a deterministic name-family sweep — never a
191
- * point-in-time `attachedTo` read, which Blaxel's autonomous idle→STANDBY detach can race
192
- * ahead of, leaving a live orphan the read cannot see (orphan-cleanup-fix.md §1/§3.3). Then
193
- * poll until the platform reports the volume detached. Safe because at sweep time the
194
- * replacement does not exist yet (its name is minted after) and any live family member is by
195
- * construction either the generation being replaced or an uncommitted leftover — the home's
196
- * durable state is the volume, never a sandbox; one wake per home runs at a time. */
197
- async sweepAndReleaseVolume(volumeId, homeId) {
198
- // Scope the list to sandboxes Hearth tagged for THIS home (externalId = volumeId), then keep
199
- // the name-family matcher as an AND-intersection on top (orphan-cleanup-fix MAJOR-1). The
200
- // destroy set requires BOTH keys to agree — the tag says "Hearth created this for home X",
201
- // the name says "it is in X's generation family" — so a single fault (a mistagged create, a
202
- // future API silently ignoring the filter param) cannot widen the blast radius back onto a
203
- // foreign home. Fail-safe defense-in-depth on a destructive op, NOT a lenient fallback.
204
- const inFamily = generationFamilyMatcher(volumeId);
205
- const doomed = (await this.provider.listMachines({ externalId: volumeId })).map((m) => m.machineId).filter(inFamily);
206
- for (const name of doomed)
207
- await this.provider.destroy(name); // sequential; destroy tolerates not_found
208
- if (doomed.length > 0)
209
- emitMarker('hearth.recreate.sweep', { home_id: homeId, volume_id: volumeId, destroyed: doomed });
210
- await this.pollVolumeDetached(volumeId, homeId);
211
- }
212
- /** Poll the volume until the platform reports it detached — the create-precondition gate
213
- * (recreate-race-fix.md §3.2). Reads only; never destroys. Emits `hearth.recreate.volume_wait`
214
- * whenever it actually had to wait, so the re-proof can quantify headroom against the budget. */
215
- async pollVolumeDetached(volumeId, homeId) {
216
- const pollStartedAt = Date.now();
217
- const deadline = pollStartedAt + VOLUME_DETACH_TIMEOUT_MS;
218
- let waited = false;
219
- for (;;) {
220
- const attached = await this.provider.getVolumeAttachment(volumeId);
221
- if (attached === null)
222
- break;
223
- waited = true;
224
- if (Date.now() >= deadline) {
225
- throw general(`volume ${volumeId} still attached to ${attached} after ${VOLUME_DETACH_TIMEOUT_MS}ms — cannot recreate home ${homeId}`, {
226
- homeId,
227
- volumeId,
228
- attached,
229
- });
230
- }
231
- await delay(VOLUME_DETACH_POLL_MS);
232
- }
233
- if (waited) {
234
- emitMarker('hearth.recreate.volume_wait', { home_id: homeId, volume_id: volumeId, waited_ms: Date.now() - pollStartedAt });
235
- }
236
- }
237
- /** Create one fresh generation and bring it to a serving state (create → bootstrap exec →
238
- * waitForPort → publish → getMachine), with provider self-cleanup and a single bounded
239
- * retry (orphan-cleanup-fix.md §3.4/§4). Each attempt destroys the generation IT created if
240
- * anything from `createMachine` through the final `getMachine` fails — no code path exits
241
- * leaving a live generation it created and did not return — then rethrows the ORIGINAL error
242
- * (cleanup never masks it). On the first failure, if the error is a genuine server-side
243
- * deployment transient, pause, re-run ONLY the volume-free poll (the sweep already ran once),
244
- * mint a NEW name, and retry exactly once; a second failure throws. The sweep is the caller's
245
- * responsibility and runs before the first attempt only. */
246
- async createGeneration(args) {
247
- const attempt = async (sandboxName) => {
248
- try {
249
- await this.provider.createMachine({
250
- tenantId: args.tenantId,
251
- name: sandboxName,
252
- // Tag EVERY generation (first-name and retry-name alike) with the home's stable volume
253
- // id so the sweep's filtered list only ever sees this home's family (MAJOR-1).
254
- externalId: args.volumeId,
255
- ...(args.image === undefined ? {} : { image: args.image }),
256
- envs: guestEnv(this.config),
257
- volumeMount: { volumeName: args.volumeId, path: this.config.guestHome },
258
- ports: [{ target: this.config.webPort, protocol: 'HTTP' }],
259
- });
260
- await this.provider.exec(sandboxName, { command: args.bootstrapScript, background: true, cwd: this.config.guestHome });
261
- await this.provider.waitForPort(sandboxName, this.config.webPort, { maxWaitMs: 60_000 });
262
- const route = await this.provider.publishHttpPort(sandboxName, {
263
- machineId: sandboxName,
264
- targetPort: this.config.webPort,
265
- routeName: stableRouteName(sandboxName),
266
- routeId: stableRouteName(sandboxName),
267
- });
268
- return { route, machine: await this.getMachine(sandboxName) };
269
- }
270
- catch (error) {
271
- // §3.4 self-cleanup: destroy the generation this attempt created; never mask the original.
272
- await this.provider
273
- .destroy(sandboxName)
274
- .catch((cleanupErr) => emitMarker('hearth.recreate.cleanup_failed', { home_id: args.homeId, sandbox: sandboxName, error: serialize(cleanupErr) }, 'warn'));
275
- throw error;
276
- }
277
- };
278
- const firstName = mintSandboxName(args.volumeId);
279
- try {
280
- return { sandboxName: firstName, ...(await attempt(firstName)) };
281
- }
282
- catch (error) {
283
- if (!isRetryableDeploymentError(error))
284
- throw error;
285
- emitMarker('hearth.recreate.create_retry', { home_id: args.homeId, discarded_generation: firstName, error: serialize(error) }, 'warn');
286
- await delay(CREATE_RETRY_DELAY_MS);
287
- // Re-run ONLY the poll — NOT a re-sweep. This sits outside attempt()'s try/catch, so a
288
- // poll-timeout here would otherwise mask the ORIGINAL deployment error (MINOR-3). The
289
- // original is what the caller must see (it is already recorded in the create_retry
290
- // marker above); on a poll failure during the retry path, rethrow it, not the timeout.
291
- try {
292
- await this.pollVolumeDetached(args.volumeId, args.homeId);
293
- }
294
- catch {
295
- throw error;
296
- }
297
- const secondName = mintSandboxName(args.volumeId);
298
- return { sandboxName: secondName, ...(await attempt(secondName)) }; // second failure throws (self-cleaned)
299
- }
300
- }
301
- /** Targeted destroy→recreate onto a SPECIFIC image (auto-upgrade roll and
302
- * rollback, design §4/§5). Unlike `wake()`'s standby-resume path, this is the ONE path
303
- * that force-destroys the home's sandboxes before recreating. Cleanup is a deterministic
304
- * name-family sweep (orphan-cleanup-fix.md §3.3): every generation of this home is destroyed
305
- * before the fresh one is minted, so a failed roll-forward generation can never survive as a
306
- * live orphan even after Blaxel autonomously detaches it from the volume. The recreate lands
307
- * on a FRESH sandbox name every call — a name that has never existed cannot 409 against a
308
- * lingering same-name delete. The durable volume is a separate resource and is NEVER touched
309
- * (flow-D durability) — user state is byte-identical across the flip, which is exactly what
310
- * makes both the upgrade and the rollback lossless. The new `providerSandboxId` is returned on
311
- * the refresh; callers persist it (`refreshProviderPointers` treats it as a refreshable
312
- * pointer). `templateVersion` is baked into the bootstrap's fail-fast version assert (the
313
- * baked crtr must match) AND reported back so the CP records the target version — the caller
314
- * still independently asserts `crtr sys version` in-guest before trusting it (wake.ts
315
- * `rollHome`/`rollbackHome`, against `refresh.providerSandboxId`, never a stale captured
316
- * name). */
317
- async recreateOnImage(homeInput, imageRef, templateVersion) {
318
- const current = ensureReadyHome(homeInput);
319
- if (imageRef.trim() === '') {
320
- throw general(`recreateOnImage for home ${current.homeId} requires a non-empty image ref`, { homeId: current.homeId });
321
- }
322
- if (templateVersion.trim() === '') {
323
- throw general(`recreateOnImage for home ${current.homeId} requires a non-empty template version`, { homeId: current.homeId });
324
- }
325
- // Sweep the whole name family (destroys the failed roll-forward generation whether or not it
326
- // still holds the volume — the exact Run-3 orphan gap) then wait until the volume is confirmed
327
- // detached before minting a fresh name and creating (orphan-cleanup-fix.md §3.3).
328
- await this.sweepAndReleaseVolume(current.providerVolumeId, current.homeId);
329
- const bootstrapScript = buildBootstrapScript({
330
- guestHome: this.config.guestHome,
331
- crtrVersion: templateVersion,
332
- relayToken: current.relayToken,
333
- webPort: this.config.webPort,
334
- nodeId: current.homeAgentTarget,
335
- piAuthJson: this.config.piAuthJson,
336
- piPackages: this.config.piPackages,
337
- isFirstProvision: false,
338
- });
339
- const { sandboxName, route, machine } = await this.createGeneration({
340
- homeId: current.homeId,
341
- tenantId: current.tenantId,
342
- volumeId: current.providerVolumeId,
343
- bootstrapScript,
344
- image: imageRef,
345
- });
346
- // Report the TARGET version, not the provider's construction-time value:
347
- // the sandbox was recreated on `imageRef`, whose baked crtr is `templateVersion`.
348
- return {
349
- ...refreshedPointers(current, this.provider, route, 'running', sandboxName, machine),
350
- templateVersion,
351
- };
352
- }
353
- async suspend(homeInput) {
354
- const current = ensureReadyHome(homeInput);
355
- const machine = await this.getMachine(current.providerSandboxId);
356
- // Snapshot-never-kill (P3.4): this method must never call `provider.destroy()` or
357
- // `provider.createMachine()` — those are the only ways the underlying sandbox is torn down
358
- // or relaunched, and neither belongs on the suspend path. Blaxel (@blaxel/core 0.2.94) has
359
- // no imperative pause/standby call to invoke here; `snapshotEnabled` (set at create time in
360
- // blaxel.ts) is the only discoverable seam, and it makes idle snapshot-pause a platform-
361
- // managed background behavior rather than something this method triggers. So suspend() does
362
- // not fabricate a 'suspended' status the way a real pause API's response would justify —
363
- // doing so would tell the CP registry a snapshot happened when nothing was asked of the
364
- // provider. It fails closed instead: assert the sandbox still exists (a missing/terminated
365
- // sandbox cannot be "suspended", it must be re-provisioned through wake()), and return the
366
- // provider's own observed status untouched so the CP can tell "confirmed paused" from
367
- // "still running, waiting on Blaxel's own idle timer" rather than trusting an invented one.
368
- if (machine === null) {
369
- throw general(`cannot suspend home ${current.homeId}: backing sandbox ${current.providerSandboxId} is not present`, {
370
- homeId: current.homeId,
371
- providerSandboxId: current.providerSandboxId,
372
- });
373
- }
374
- return refreshedPointers(current, this.provider, null, machine.state, current.providerSandboxId, machine);
375
- }
376
- async getMachine(machineId) {
377
- const machine = await this.provider.getMachine(machineId);
378
- return machine.state === 'terminated' ? null : machine;
379
- }
380
- /** P2.6 stdin-capable exec seam, passed through verbatim — the `HomeBackend` interface is the
381
- * only door CP code uses to reach it (see `provider.ts`), so callers never import
382
- * `BlaxelMachineProvider` directly. */
383
- async exec(machineId, input) {
384
- return this.provider.exec(machineId, input);
385
- }
386
- }
@@ -1,36 +0,0 @@
1
- import type { MachineRef, RouteRef } from '../types.js';
2
- import type { CreateMachineInput, DestroyOutcome, ExecInput, ExecResult, HealthResult, MachineProvider, PublishHttpPortInput, VolumeRef } from './types.js';
3
- interface BlaxelMachineProviderOptions {
4
- region?: string;
5
- memory?: number;
6
- image?: string;
7
- templateVersion?: string;
8
- volumeSizeGb?: number;
9
- }
10
- export declare function isMissingResourceError(error: unknown): boolean;
11
- export declare class BlaxelMachineProvider implements MachineProvider {
12
- readonly providerName: "blaxel";
13
- readonly imageRef: string;
14
- readonly templateVersion: string;
15
- readonly volumeSizeGb: number;
16
- private readonly config;
17
- constructor(options?: BlaxelMachineProviderOptions);
18
- createVolume(input: {
19
- name: string;
20
- }): Promise<VolumeRef>;
21
- createMachine(input: CreateMachineInput): Promise<MachineRef>;
22
- listMachines(filter?: {
23
- externalId?: string;
24
- }): Promise<MachineRef[]>;
25
- getMachine(machineId: string): Promise<MachineRef>;
26
- exec(machineId: string, input: ExecInput): Promise<ExecResult>;
27
- waitForPort(machineId: string, port: number, opts?: {
28
- maxWaitMs?: number;
29
- }): Promise<void>;
30
- publishHttpPort(machineId: string, input: PublishHttpPortInput): Promise<RouteRef>;
31
- health(): Promise<HealthResult>;
32
- destroy(machineId: string): Promise<DestroyOutcome>;
33
- destroyVolume(volumeId: string): Promise<DestroyOutcome>;
34
- getVolumeAttachment(volumeId: string): Promise<string | null>;
35
- }
36
- export {};