@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22

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 (69) hide show
  1. package/dist/bin/evolver-llm-proxy.js +0 -0
  2. package/dist/bin/evolver-proxy.d.ts +105 -7
  3. package/dist/bin/evolver-proxy.js +877 -121
  4. package/dist/daemon/atpConsent.js +5 -2
  5. package/dist/daemon/collaborationFacade.js +26 -16
  6. package/dist/daemon/proxyDaemon.d.ts +65 -0
  7. package/dist/daemon/proxyDaemon.js +1384 -29
  8. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  9. package/dist/daemon/publishRecallVerifier.js +495 -0
  10. package/dist/daemon/selectHub.js +5 -3
  11. package/dist/daemon/systemdNotifier.d.ts +48 -0
  12. package/dist/daemon/systemdNotifier.js +163 -0
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.js +4 -1
  15. package/dist/lifecycle/claimNudge.d.ts +20 -0
  16. package/dist/lifecycle/claimNudge.js +124 -0
  17. package/dist/lifecycle/legacyNodeId.d.ts +11 -13
  18. package/dist/lifecycle/legacyNodeId.js +35 -20
  19. package/dist/lifecycle/manager.d.ts +4 -0
  20. package/dist/lifecycle/manager.js +15 -2
  21. package/dist/llm/server.js +24 -4
  22. package/dist/llm/traceControl.js +1 -1
  23. package/dist/llm/upstream.d.ts +5 -1
  24. package/dist/llm/upstream.js +72 -2
  25. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  26. package/dist/private/accountAssetCompatibility.js +196 -0
  27. package/dist/private/adapterLoader.d.ts +21 -1
  28. package/dist/private/adapterLoader.js +242 -7
  29. package/dist/private/nodeCredentialStore.d.ts +23 -0
  30. package/dist/private/nodeCredentialStore.js +210 -0
  31. package/dist/router/messagesRoute.js +9 -3
  32. package/dist/router/providerRoutes.js +7 -3
  33. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  34. package/dist/selfUpdate/bootstrap.js +3524 -0
  35. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  36. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  37. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  38. package/dist/selfUpdate/builtinKey.js +16 -0
  39. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  40. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  41. package/dist/selfUpdate/executor.d.ts +27 -11
  42. package/dist/selfUpdate/executor.js +233 -58
  43. package/dist/selfUpdate/failureCodes.d.ts +10 -0
  44. package/dist/selfUpdate/failureCodes.js +13 -0
  45. package/dist/selfUpdate/index.d.ts +5 -1
  46. package/dist/selfUpdate/index.js +5 -1
  47. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  48. package/dist/selfUpdate/lastUpdate.js +37 -6
  49. package/dist/selfUpdate/migration.d.ts +158 -0
  50. package/dist/selfUpdate/migration.js +2672 -0
  51. package/dist/selfUpdate/policy.d.ts +19 -2
  52. package/dist/selfUpdate/policy.js +76 -2
  53. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  54. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  55. package/dist/selfUpdate/releaseBinary.d.ts +13 -0
  56. package/dist/selfUpdate/releaseBinary.js +93 -10
  57. package/dist/selfUpdate/transaction.d.ts +117 -0
  58. package/dist/selfUpdate/transaction.js +1322 -0
  59. package/dist/selfUpdate/unixController.d.ts +23 -0
  60. package/dist/selfUpdate/unixController.js +514 -0
  61. package/dist/selfUpdate/version.d.ts +6 -2
  62. package/dist/selfUpdate/version.js +5 -3
  63. package/dist/selfUpdate/windowsController.d.ts +35 -0
  64. package/dist/selfUpdate/windowsController.js +655 -0
  65. package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
  66. package/dist/selfUpdate/windowsUpdater.js +882 -0
  67. package/dist/sync/engine.d.ts +12 -0
  68. package/dist/sync/engine.js +255 -64
  69. package/package.json +10 -3
@@ -4,15 +4,16 @@
4
4
  // shells out or touches a real install tree by accident.
5
5
  //
6
6
  // THE HARD GATE (verify-before-apply, non-negotiable): after download and BEFORE any filesystem write or restart,
7
- // the executor calls the PURE core verifyManifest. If verification fails — bad sha256, missing/invalid signature
7
+ // the executor calls the PURE core verifySelectedManifestArtifact. If verification fails — bad sha256,
8
+ // missing/invalid signature
8
9
  // when a key is configured, anything — the executor writes NOTHING, restarts NOTHING, and returns a structured
9
10
  // failure. The old version stays intact and runnable. A compromised hub cannot turn this channel into fleet RCE.
10
11
  //
11
12
  // Concurrency: a process-level mutex guarantees that concurrent force_update messages execute the update EXACTLY
12
13
  // once. The second caller short-circuits with `already_in_progress` and performs no I/O.
13
14
  import { ops } from '@evomap/evolver-core';
14
- import { SELF_UPDATE_FAILURE_CODES, classifySelfUpdateError, codeForDecisionReject, } from './failureCodes.js';
15
- const { decideUpdate, verifyManifest } = ops;
15
+ import { SELF_UPDATE_FAILURE_CODES, classifySelfUpdateError, codeForDecisionReject, selfUpdateFailure, } from './failureCodes.js';
16
+ const { decideUpdate, verifySelectedManifestArtifact } = ops;
16
17
  // Process-level mutex. Module scope is correct: there is one daemon per process, and v1's _forceUpdateInFlight had
17
18
  // the same lifetime. Guards against two force_update envelopes (or a heartbeat-driven + mailbox-driven trigger)
18
19
  // racing the same upgrade and replacing files twice / double-restarting.
@@ -31,22 +32,28 @@ function report(deps, result) {
31
32
  return result;
32
33
  }
33
34
  /**
34
- * Execute a force_update directive end to end: decide(mutex) → download → VERIFY → atomic replace → restart.
35
+ * Execute a force_update directive end to end: fast exits leaseresolve/decide → download → VERIFY → replace → restart.
35
36
  *
36
37
  * Order is load-bearing:
37
- * 1. policy off → do nothing (the default-off risk gate; a half-built channel must not auto-apply).
38
- * 2. decideUpdate (pure): reject bad manifests, NOOP when already satisfied (no download, no restart).
39
- * 3. mutex: exactly one execution; concurrent callers get `already_in_progress` and touch no disk.
40
- * 4. download the staged release.
41
- * 5. verifyManifest (pure) THE GATE. Fail → no write, no restart, `rejected_verification`.
42
- * 6. atomicReplace, then restart(). Only reached after verification passed.
38
+ * 1. policy off → do nothing (explicit opt-out; auto is hard-gated upstream by supervisor + public key).
39
+ * 2. reject malformed/already-satisfied required versions without acquiring the lifecycle owner lease.
40
+ * 3. mutex + lifecycle owner lease: exactly one executor may resolve or mutate release state.
41
+ * 4. resolve and decideUpdate (pure): reject bad manifests or NOOP under the held lease.
42
+ * 5. download the staged release.
43
+ * 6. verifySelectedManifestArtifact (pure) — THE GATE. Fail no write, no restart.
44
+ * 7. atomicReplace, then restart(). Only reached after verification passed.
43
45
  *
44
46
  * Never throws: every failure becomes a structured SelfUpdateResult so the daemon can report it and keep running
45
47
  * on the old (intact) version.
46
48
  */
47
49
  export async function executeForceUpdate(directive, deps) {
50
+ let selectedResult;
51
+ const finish = (result) => {
52
+ selectedResult = result;
53
+ return report(deps, result);
54
+ };
48
55
  if (deps.policy !== 'auto') {
49
- return report(deps, {
56
+ return finish({
50
57
  outcome: 'disabled',
51
58
  reason: deps.policy === 'prompt' ? 'self_update_policy_prompt_requires_approval' : 'self_update_policy_off',
52
59
  });
@@ -55,87 +62,202 @@ export async function executeForceUpdate(directive, deps) {
55
62
  ? ops.normalizeRequiredVersion(directive.required_version)
56
63
  : undefined;
57
64
  if (directive.required_version !== undefined && !requiredFloor) {
58
- return report(deps, {
65
+ return finish({
59
66
  outcome: 'rejected_decision',
60
67
  reason: 'required_version_invalid',
61
68
  failureCode: SELF_UPDATE_FAILURE_CODES.BAD_REQUIRED_VERSION,
62
69
  });
63
70
  }
64
71
  if (requiredFloor && ops.currentSatisfiesRequiredVersion(deps.currentVersion, requiredFloor)) {
65
- return report(deps, { outcome: 'noop', reason: 'already_satisfied', targetVersion: requiredFloor });
72
+ return finish({ outcome: 'noop', reason: 'already_satisfied', targetVersion: requiredFloor });
66
73
  }
67
- let manifest = directive.manifest;
68
- if (deps.resolveManifest && shouldResolveManifest(directive, manifest, Boolean(deps.publicKey))) {
69
- try {
70
- manifest = await deps.resolveManifest(directive, deps.currentVersion);
71
- }
72
- catch (err) {
73
- const targetVersion = ops.normalizeRequiredVersion(directive.required_version);
74
- const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
75
- return report(deps, {
76
- outcome: 'download_failed',
77
- reason: `manifest_resolve_failed: ${classified.detail}`,
78
- failureCode: classified.failureCode,
79
- ...(targetVersion ? { targetVersion } : {}),
80
- });
81
- }
82
- }
83
- const effectiveDirective = { ...directive, manifest };
84
- const decision = decideUpdate({
85
- current: deps.currentVersion,
86
- ...(requiredFloor ? { required: requiredFloor } : {}),
87
- manifest,
88
- });
89
- if (decision.action === 'reject') {
90
- return report(deps, {
91
- outcome: 'rejected_decision',
92
- reason: decision.reason,
93
- failureCode: codeForDecisionReject(decision.reason),
94
- ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
95
- });
96
- }
97
- if (decision.action === 'noop') {
98
- return report(deps, { outcome: 'noop', reason: decision.reason, ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}) });
99
- }
100
- // action === 'proceed'. Take the mutex; a concurrent force_update short-circuits here with NO I/O.
74
+ // The mutex and lifecycle lease precede manifest resolution, which is release I/O too.
75
+ // Policy, malformed required-version, and already-satisfied fast exits remain lock-free.
101
76
  if (inFlight) {
102
- return report(deps, { outcome: 'already_in_progress', reason: 'another_update_in_flight' });
77
+ return finish({ outcome: 'already_in_progress', reason: 'another_update_in_flight' });
103
78
  }
104
79
  inFlight = true;
105
- const targetVersion = decision.targetVersion ?? manifest?.version ?? '';
80
+ let targetVersion = requiredFloor ?? '';
81
+ let lifecycleLease;
82
+ let transaction;
83
+ const assertLifecycleLease = async () => {
84
+ try {
85
+ await lifecycleLease?.assertOwned();
86
+ }
87
+ catch (error) {
88
+ const classified = classifySelfUpdateError(error, SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED);
89
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `self_update_lifecycle_owner_lease_lost:${classified.detail}`, { cause: error });
90
+ }
91
+ };
106
92
  try {
93
+ if (deps.acquireLifecycleLease) {
94
+ try {
95
+ lifecycleLease = await deps.acquireLifecycleLease();
96
+ await assertLifecycleLease();
97
+ }
98
+ catch (err) {
99
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED);
100
+ return finish({
101
+ outcome: classified.failureCode === SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED
102
+ ? 'already_in_progress'
103
+ : 'replace_failed',
104
+ reason: classified.detail,
105
+ failureCode: classified.failureCode,
106
+ ...(targetVersion ? { targetVersion } : {}),
107
+ });
108
+ }
109
+ }
110
+ let manifest = directive.manifest;
111
+ if (deps.resolveManifest && shouldResolveManifest(directive, manifest, Boolean(deps.publicKey))) {
112
+ try {
113
+ await assertLifecycleLease();
114
+ manifest = await deps.resolveManifest(directive, deps.currentVersion);
115
+ }
116
+ catch (err) {
117
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
118
+ if (classified.failureCode === SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED) {
119
+ return finish({
120
+ outcome: 'replace_failed',
121
+ reason: classified.detail,
122
+ failureCode: classified.failureCode,
123
+ ...(targetVersion ? { targetVersion } : {}),
124
+ });
125
+ }
126
+ return finish({
127
+ outcome: 'download_failed',
128
+ reason: `manifest_resolve_failed: ${classified.detail}`,
129
+ failureCode: classified.failureCode,
130
+ ...(targetVersion ? { targetVersion } : {}),
131
+ });
132
+ }
133
+ }
134
+ const effectiveDirective = { ...directive, manifest };
135
+ const decision = decideUpdate({
136
+ current: deps.currentVersion,
137
+ ...(requiredFloor ? { required: requiredFloor } : {}),
138
+ manifest,
139
+ });
140
+ if (decision.action === 'reject') {
141
+ return finish({
142
+ outcome: 'rejected_decision',
143
+ reason: decision.reason,
144
+ failureCode: codeForDecisionReject(decision.reason),
145
+ ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
146
+ });
147
+ }
148
+ if (decision.action === 'noop') {
149
+ return finish({
150
+ outcome: 'noop',
151
+ reason: decision.reason,
152
+ ...(decision.targetVersion ? { targetVersion: decision.targetVersion } : {}),
153
+ });
154
+ }
155
+ targetVersion = decision.targetVersion ?? manifest?.version ?? '';
156
+ if (deps.beginTransaction) {
157
+ try {
158
+ await assertLifecycleLease();
159
+ transaction = await deps.beginTransaction(targetVersion);
160
+ }
161
+ catch (err) {
162
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED);
163
+ return finish({
164
+ outcome: classified.failureCode === SELF_UPDATE_FAILURE_CODES.UPDATE_LOCKED ? 'already_in_progress' : 'replace_failed',
165
+ reason: classified.detail,
166
+ failureCode: classified.failureCode,
167
+ targetVersion,
168
+ });
169
+ }
170
+ }
107
171
  // 4. Download the staged release.
108
172
  let dl;
109
173
  try {
174
+ await assertLifecycleLease();
110
175
  dl = await deps.download(targetVersion, effectiveDirective);
111
176
  }
112
177
  catch (err) {
113
178
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.DOWNLOAD_FAILED);
114
- return report(deps, {
179
+ await transaction?.abort(classified.failureCode).catch(() => { });
180
+ if (classified.failureCode === SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED) {
181
+ return finish({
182
+ outcome: 'replace_failed',
183
+ reason: classified.detail,
184
+ failureCode: classified.failureCode,
185
+ targetVersion,
186
+ });
187
+ }
188
+ return finish({
115
189
  outcome: 'download_failed',
116
190
  reason: classified.detail,
117
191
  failureCode: classified.failureCode,
118
192
  targetVersion,
119
193
  });
120
194
  }
195
+ if (transaction) {
196
+ try {
197
+ await assertLifecycleLease();
198
+ dl = await transaction.adoptDownloaded(dl);
199
+ }
200
+ catch (err) {
201
+ await transaction.abort(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED).catch(() => { });
202
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
203
+ return finish({
204
+ outcome: 'replace_failed',
205
+ reason: classified.detail,
206
+ failureCode: classified.failureCode,
207
+ targetVersion,
208
+ });
209
+ }
210
+ }
121
211
  // 5. THE GATE: verify the downloaded bytes against the (optionally signed) manifest BEFORE any write.
122
- const verification = verifyManifest(manifest, dl.artifacts, ...(deps.publicKey ? [deps.publicKey] : []));
212
+ try {
213
+ await assertLifecycleLease();
214
+ }
215
+ catch (err) {
216
+ await transaction?.abort(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED).catch(() => { });
217
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED);
218
+ return finish({
219
+ outcome: 'replace_failed',
220
+ reason: classified.detail,
221
+ failureCode: classified.failureCode,
222
+ targetVersion,
223
+ });
224
+ }
225
+ const verification = verifySelectedManifestArtifact(manifest, dl.artifacts, ...(deps.publicKey ? [deps.publicKey] : []));
123
226
  if (!verification.ok) {
227
+ await transaction?.abort(SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION).catch(() => { });
124
228
  // Verification failed → write NOTHING, restart NOTHING. Old version stays intact and runnable.
125
- return report(deps, {
229
+ return finish({
126
230
  outcome: 'rejected_verification',
127
231
  reason: verification.reason,
128
232
  failureCode: SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION,
129
233
  targetVersion,
130
234
  });
131
235
  }
236
+ try {
237
+ await assertLifecycleLease();
238
+ await transaction?.markVerified(dl.artifacts);
239
+ }
240
+ catch (err) {
241
+ await transaction?.abort(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED).catch(() => { });
242
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED);
243
+ return finish({
244
+ outcome: 'replace_failed',
245
+ reason: classified.detail,
246
+ failureCode: classified.failureCode,
247
+ targetVersion,
248
+ });
249
+ }
132
250
  // 6. Verified. Atomic replace, then signal restart. A replace failure leaves the old version intact.
133
251
  try {
134
- await deps.atomicReplace(dl.stagedPath);
252
+ await assertLifecycleLease();
253
+ if (transaction)
254
+ await transaction.install();
255
+ else
256
+ await deps.atomicReplace(dl.stagedPath);
135
257
  }
136
258
  catch (err) {
137
259
  const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.COPY_FAILED);
138
- return report(deps, {
260
+ return finish({
139
261
  outcome: 'replace_failed',
140
262
  reason: classified.detail,
141
263
  failureCode: classified.failureCode,
@@ -147,17 +269,70 @@ export async function executeForceUpdate(directive, deps) {
147
269
  reason: 'verified_and_replaced',
148
270
  targetVersion,
149
271
  appliedVia: dl.appliedVia ?? 'binary',
272
+ ...(transaction ? { confirmationPending: true } : {}),
150
273
  };
151
- report(deps, result);
152
- deps.restart(); // v1 convention: exit(78) → supervisor relaunches the new version.
274
+ finish(result);
275
+ try {
276
+ await assertLifecycleLease();
277
+ await transaction?.markRestartRequested();
278
+ await assertLifecycleLease();
279
+ await deps.restart(); // v1 convention: exit(78) → supervisor relaunches the new version.
280
+ }
281
+ catch (err) {
282
+ const classified = classifySelfUpdateError(err, SELF_UPDATE_FAILURE_CODES.RESTART_FAILED);
283
+ try {
284
+ await transaction?.rollback(classified.failureCode);
285
+ }
286
+ catch (rollbackError) {
287
+ const rollback = classifySelfUpdateError(rollbackError, SELF_UPDATE_FAILURE_CODES.ROLLBACK_FAILED);
288
+ return finish({
289
+ outcome: 'rollback_failed',
290
+ reason: rollback.detail,
291
+ failureCode: rollback.failureCode,
292
+ targetVersion,
293
+ });
294
+ }
295
+ return finish({
296
+ outcome: 'restart_failed',
297
+ reason: classified.detail,
298
+ failureCode: classified.failureCode,
299
+ targetVersion,
300
+ });
301
+ }
153
302
  return result;
154
303
  }
155
304
  finally {
156
- // Released so a later legitimate update (after a failed attempt) can proceed. On the success path the process
157
- // is exiting anyway; releasing is harmless and keeps the mutex honest if restart() is a test fake that returns.
305
+ await transaction?.release().catch(() => { });
306
+ if (lifecycleLease) {
307
+ try {
308
+ await lifecycleLease.release();
309
+ }
310
+ catch (error) {
311
+ if (selectedResult) {
312
+ selectedResult.cleanupWarning = boundedCleanupWarning(error);
313
+ if (selectedResult.outcome === 'applied') {
314
+ selectedResult.reason = 'verified_and_replaced_lifecycle_lease_release_unconfirmed';
315
+ }
316
+ try {
317
+ deps.onCleanupWarning?.(selectedResult.cleanupWarning, selectedResult);
318
+ }
319
+ catch {
320
+ // Cleanup reporting must not replace the primary update outcome.
321
+ }
322
+ }
323
+ }
324
+ }
158
325
  inFlight = false;
159
326
  }
160
327
  }
328
+ function boundedCleanupWarning(error) {
329
+ const raw = error instanceof Error ? error.message : String(error);
330
+ const normalized = raw
331
+ .replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ')
332
+ .replace(/\s+/g, ' ')
333
+ .trim();
334
+ return (normalized || 'self_update_lifecycle_owner_lease_release_failed').slice(0, 512);
335
+ }
161
336
  function shouldResolveManifest(directive, manifest, signatureRequired) {
162
337
  if (manifest === undefined)
163
338
  return true;
@@ -17,6 +17,16 @@ export declare const SELF_UPDATE_FAILURE_CODES: Readonly<{
17
17
  readonly FALLBACK_DOWNLOAD_FAILED: "fallback_download_failed";
18
18
  readonly FALLBACK_EXTRACT_FAILED: "fallback_extract_failed";
19
19
  readonly FALLBACK_MISSING_BINARY: "fallback_missing_binary";
20
+ readonly UPDATE_LOCKED: "update_locked";
21
+ readonly RECOVERY_REQUIRED: "recovery_required";
22
+ readonly UNSAFE_UPDATE_PATH: "unsafe_update_path";
23
+ readonly RESTART_FAILED: "restart_failed";
24
+ readonly READ_BACK_FAILED: "read_back_failed";
25
+ readonly ROLLBACK_FAILED: "rollback_failed";
26
+ readonly MIGRATION_DOWNLOAD_FAILED: "migration_download_failed";
27
+ readonly MIGRATION_VERIFY_FAILED: "migration_verify_failed";
28
+ readonly MIGRATION_INSTALL_FAILED: "migration_install_failed";
29
+ readonly MIGRATION_REGISTER_FAILED: "migration_register_failed";
20
30
  }>;
21
31
  export type SelfUpdateFailureCode = typeof SELF_UPDATE_FAILURE_CODES[keyof typeof SELF_UPDATE_FAILURE_CODES];
22
32
  export interface ClassifiedSelfUpdateError {
@@ -22,6 +22,19 @@ export const SELF_UPDATE_FAILURE_CODES = Object.freeze({
22
22
  FALLBACK_DOWNLOAD_FAILED: 'fallback_download_failed',
23
23
  FALLBACK_EXTRACT_FAILED: 'fallback_extract_failed',
24
24
  FALLBACK_MISSING_BINARY: 'fallback_missing_binary',
25
+ UPDATE_LOCKED: 'update_locked',
26
+ RECOVERY_REQUIRED: 'recovery_required',
27
+ UNSAFE_UPDATE_PATH: 'unsafe_update_path',
28
+ RESTART_FAILED: 'restart_failed',
29
+ READ_BACK_FAILED: 'read_back_failed',
30
+ ROLLBACK_FAILED: 'rollback_failed',
31
+ // One-time npm/JS → standalone binary migration (migration.ts). Append-only:
32
+ // these surface in attempt-marker detail strings and operator messages so
33
+ // telemetry can attribute first-run migration failures by phase.
34
+ MIGRATION_DOWNLOAD_FAILED: 'migration_download_failed',
35
+ MIGRATION_VERIFY_FAILED: 'migration_verify_failed',
36
+ MIGRATION_INSTALL_FAILED: 'migration_install_failed',
37
+ MIGRATION_REGISTER_FAILED: 'migration_register_failed',
25
38
  });
26
39
  export class SelfUpdateFailureError extends Error {
27
40
  failureCode;
@@ -2,4 +2,8 @@ export * from './executor.js';
2
2
  export * from './version.js';
3
3
  export * from './policy.js';
4
4
  export * from './releaseBinary.js';
5
- export * from './failureCodes.js';
5
+ export * from './transaction.js';
6
+ export * from './failureCodes.js';
7
+ export * from './unixController.js';
8
+ export * from './windowsController.js';
9
+ export * from './bootstrapReadiness.js';
@@ -2,4 +2,8 @@ export * from './executor.js';
2
2
  export * from './version.js';
3
3
  export * from './policy.js';
4
4
  export * from './releaseBinary.js';
5
- export * from './failureCodes.js';
5
+ export * from './transaction.js';
6
+ export * from './failureCodes.js';
7
+ export * from './unixController.js';
8
+ export * from './windowsController.js';
9
+ export * from './bootstrapReadiness.js';
@@ -1,5 +1,6 @@
1
1
  import { mailbox } from '@evomap/evolver-core';
2
2
  import type { ForceUpdateDirective, SelfUpdateResult } from './executor.js';
3
+ import type { SelfUpdateRecoveryResult } from './transaction.js';
3
4
  type MailboxStore = mailbox.MailboxStore;
4
5
  type LastUpdateStatus = 'success' | 'failed' | 'skipped' | 'pending';
5
6
  export interface LastUpdatePayload {
@@ -12,7 +13,7 @@ export interface LastUpdatePayload {
12
13
  /**
13
14
  * Which download channel produced the bytes that got applied: `'binary'` is
14
15
  * the precompiled-asset happy path, `'tarball'` means Channel 1b fallback
15
- * (release `.tar.gz`) was used. Persisted on success so the hub can see
16
+ * (release `.tar.gz`) was used. Persisted while confirmation is pending and on success so the hub can see
16
17
  * "primary CDN is degraded — fallback carrying production" without having
17
18
  * to mine telemetry. Absent on non-success or when the executor predates
18
19
  * the appliedVia field.
@@ -36,6 +37,7 @@ export declare function reportPendingSelfUpdateLastUpdate(store: MailboxStore, d
36
37
  fromVersion: string;
37
38
  now?: number;
38
39
  }): boolean;
40
+ export declare function finalizeSelfUpdateRecoveryLastUpdate(store: MailboxStore, recovery: SelfUpdateRecoveryResult, now?: number): boolean;
39
41
  export declare function lastUpdateFromSelfUpdateResult(directive: ForceUpdateDirective, result: SelfUpdateResult, opts: {
40
42
  fromVersion: string;
41
43
  now: number;
@@ -86,6 +86,37 @@ export function reportPendingSelfUpdateLastUpdate(store, directive, opts = { fro
86
86
  ...(directive.directive_id ? { directive_id: String(directive.directive_id) } : {}),
87
87
  }, now);
88
88
  }
89
+ export function finalizeSelfUpdateRecoveryLastUpdate(store, recovery, now = Date.now()) {
90
+ if (recovery.outcome !== 'confirmed'
91
+ && recovery.outcome !== 'rolled_back'
92
+ && recovery.outcome !== 'blocked')
93
+ return false;
94
+ const toVersion = concreteVersion(recovery.targetVersion);
95
+ const fromVersion = concreteVersion(recovery.fromVersion);
96
+ if (!toVersion)
97
+ return false;
98
+ const current = readPendingLastUpdate(store, now);
99
+ if (current && current.to_version !== toVersion)
100
+ return false;
101
+ const common = {
102
+ to_version: toVersion,
103
+ finished_at: Math.max(now, FINISHED_AT_MIN_MS),
104
+ ...(current?.directive_id ? { directive_id: current.directive_id } : {}),
105
+ ...(current?.from_version ? { from_version: current.from_version } : fromVersion ? { from_version: fromVersion } : {}),
106
+ };
107
+ if (recovery.outcome === 'confirmed') {
108
+ return writeLastUpdate(store, {
109
+ ...common,
110
+ status: 'success',
111
+ ...(current?.applied_via ? { applied_via: current.applied_via } : {}),
112
+ }, now);
113
+ }
114
+ return writeLastUpdate(store, {
115
+ ...common,
116
+ status: 'failed',
117
+ error: clampString(hubNs.redactString(`${recovery.failureCode ?? 'self_update_recovery_failed'}: ${recovery.outcome}`), LAST_UPDATE_ERROR_MAX),
118
+ }, now);
119
+ }
89
120
  export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
90
121
  if (result.outcome === 'already_in_progress' || result.outcome === 'disabled')
91
122
  return undefined;
@@ -94,11 +125,11 @@ export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
94
125
  return undefined;
95
126
  const base = {
96
127
  to_version: toVersion,
97
- status: statusForOutcome(result.outcome),
128
+ status: statusForResult(result),
98
129
  finished_at: Math.max(opts.now, FINISHED_AT_MIN_MS),
99
130
  ...(directive.directive_id ? { directive_id: String(directive.directive_id) } : {}),
100
131
  };
101
- if (base.status === 'success') {
132
+ if (base.status === 'success' || base.status === 'pending') {
102
133
  return {
103
134
  ...base,
104
135
  from_version: clampString(opts.fromVersion, LAST_UPDATE_FROM_VERSION_MAX),
@@ -114,10 +145,10 @@ export function lastUpdateFromSelfUpdateResult(directive, result, opts) {
114
145
  }
115
146
  return base;
116
147
  }
117
- function statusForOutcome(outcome) {
118
- if (outcome === 'applied')
119
- return 'success';
120
- if (outcome === 'noop')
148
+ function statusForResult(result) {
149
+ if (result.outcome === 'applied')
150
+ return result.confirmationPending ? 'pending' : 'success';
151
+ if (result.outcome === 'noop')
121
152
  return 'skipped';
122
153
  return 'failed';
123
154
  }