@atolis-hq/wake 0.4.43 → 0.4.44

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.
@@ -16,6 +16,7 @@ export interface SourceUpdatePort {
16
16
  * non-sandbox self-updates.
17
17
  */
18
18
  export interface SelfUpdateRolloutPort {
19
+ prepare(tag: string): Promise<void>;
19
20
  deploy(tag: string): Promise<void>;
20
21
  rollback(tag: string): Promise<void>;
21
22
  recordFailure(tag: string, error: unknown): Promise<void>;
@@ -27,6 +28,7 @@ export interface SelfUpdateRolloutPort {
27
28
  * durably record every supplied Run's maintenance request.
28
29
  */
29
30
  export interface SelfUpdateQuiescePort {
31
+ read?(): Promise<UpdateMaintenanceState | null>;
30
32
  acquire(tag: string, retryFailed?: boolean): Promise<UpdateMaintenanceState>;
31
33
  exclusive?<Value>(tag: string, retryFailed: boolean, operation: (state: UpdateMaintenanceState) => Promise<Value>): Promise<Value>;
32
34
  activeRuns(): Promise<readonly {
@@ -59,6 +59,11 @@ async function currentTag(input) {
59
59
  return (await input.source.currentTag?.()) ?? (await input.ledger.read());
60
60
  }
61
61
  async function startUpdateAttempt(input, tag, force) {
62
+ const existing = await input.quiesce?.read?.();
63
+ if (existing === null ||
64
+ existing === undefined ||
65
+ existing.phase === UpdateMaintenancePhase.Failed)
66
+ await prepareCandidate(input, tag);
62
67
  if (input.quiesce?.exclusive !== undefined)
63
68
  return input.quiesce.exclusive(tag, force, (lease) => updateAttempt(input, tag, force, lease));
64
69
  return updateAttempt(input, tag, force, await input.quiesce?.acquire(tag, force));
@@ -66,24 +71,30 @@ async function startUpdateAttempt(input, tag, force) {
66
71
  async function updateAttempt(input, tag, force, lease) {
67
72
  const maintenanceTag = lease?.tag ?? tag;
68
73
  const attemptId = lease?.attemptId;
69
- const context = {};
74
+ const context = {
75
+ activationStarted: false,
76
+ rollbackSucceeded: false,
77
+ recoveryRequired: false,
78
+ recoveryVerified: false,
79
+ };
70
80
  try {
71
81
  return await performUpdateAttempt(input, tag, force, lease, context);
72
82
  }
73
83
  catch (error) {
74
- await recordUpdateFailure(input, maintenanceTag, attemptId, context.deployError ?? error, error);
84
+ await recordUpdateFailure(input, maintenanceTag, attemptId, context.deployError ?? error, error, context);
75
85
  throw error;
76
86
  }
77
87
  }
78
88
  async function performUpdateAttempt(input, tag, force, lease, context) {
79
89
  if (lease !== undefined && requiresMaintenanceRecovery(lease)) {
90
+ context.recoveryRequired = true;
80
91
  await resumePendingMaintenance(input, lease);
81
92
  return false;
82
93
  }
83
94
  if (!isStartableMaintenanceLease(lease, tag))
84
95
  return false;
85
96
  await quiesceForUpdate(input);
86
- await recoverPendingUpdate(input.ledger, input.source);
97
+ await recoverPendingUpdate(input.ledger, input.source, context);
87
98
  if (!force && (await input.ledger.read()) === tag) {
88
99
  await input.quiesce?.clear(lease?.attemptId);
89
100
  return false;
@@ -116,13 +127,18 @@ async function runForwardUpdate(input, tag, attemptId, context) {
116
127
  readLedger: input.ledger.read,
117
128
  writeLedger: input.ledger.write,
118
129
  update: async (nextTag) => {
119
- await input.source.checkout(nextTag);
130
+ if (input.rollout === undefined)
131
+ await input.source.checkout(nextTag);
132
+ context.activationStarted = true;
120
133
  context.deployError = await deployRollout(input.rollout, nextTag);
121
134
  },
122
135
  health: async () => context.deployError === undefined && input.source.healthy(),
123
136
  rollback: async (priorTag) => {
124
137
  await rollbackUpdate(input, priorTag, attemptId);
138
+ if (!(await input.source.healthy()))
139
+ throw new Error(`Self-update rollback could not verify the prior tag ${priorTag}`);
125
140
  rollbackSucceeded = true;
141
+ context.rollbackSucceeded = true;
126
142
  },
127
143
  });
128
144
  }
@@ -162,7 +178,18 @@ async function rollbackUpdate(input, priorTag, attemptId) {
162
178
  await input.source.checkout(priorTag);
163
179
  await input.rollout?.rollback(priorTag);
164
180
  }
165
- async function recordUpdateFailure(input, tag, attemptId, rolloutError, originalError) {
181
+ async function recordUpdateFailure(input, tag, attemptId, rolloutError, originalError, context) {
182
+ if (context.rollbackSucceeded ||
183
+ (!context.activationStarted && (!context.recoveryRequired || context.recoveryVerified))) {
184
+ try {
185
+ await recordRolloutFailure(input.rollout, tag, rolloutError);
186
+ await input.ledger.recordBad(tag);
187
+ }
188
+ finally {
189
+ await input.quiesce?.clear(attemptId);
190
+ }
191
+ return;
192
+ }
166
193
  try {
167
194
  await input.quiesce?.fail(originalError, attemptId);
168
195
  }
@@ -184,6 +211,24 @@ async function recordRolloutFailure(rollout, tag, error) {
184
211
  // Best-effort: a failure-log write must never mask the original update failure.
185
212
  }
186
213
  }
214
+ async function prepareCandidate(input, tag) {
215
+ if (!(await input.source.isClean()))
216
+ throw new Error('Self-update requires a clean source checkout');
217
+ if (input.rollout === undefined)
218
+ return;
219
+ const priorTag = await input.ledger.read();
220
+ try {
221
+ await input.source.checkout(tag);
222
+ await input.rollout.prepare(tag);
223
+ }
224
+ catch (error) {
225
+ if (priorTag !== null)
226
+ await input.source.checkout(priorTag);
227
+ await recordRolloutFailure(input.rollout, tag, error);
228
+ await input.ledger.recordBad(tag);
229
+ throw new UpdateCandidateFailure(error);
230
+ }
231
+ }
187
232
  /**
188
233
  * A restart must never repeat a forward deploy. The persisted phase tells us
189
234
  * that source/rollout work may already have begun, so recover the prior
@@ -210,11 +255,13 @@ async function quiesceActiveRuns(quiesce) {
210
255
  function formatError(error) {
211
256
  return error instanceof Error ? error.message : String(error);
212
257
  }
213
- async function recoverPendingUpdate(ledger, source) {
258
+ async function recoverPendingUpdate(ledger, source, context) {
214
259
  const priorTag = await ledger.recover();
215
260
  if (priorTag === null)
216
261
  return;
262
+ context.recoveryRequired = true;
217
263
  await source.checkout(priorTag);
218
264
  if (!(await source.healthy()))
219
265
  throw new Error(`Self-update recovery could not verify the prior tag ${priorTag}`);
266
+ context.recoveryVerified = true;
220
267
  }
@@ -400,6 +400,7 @@ async function activeExecutionRunIds(root) {
400
400
  /** The production maintenance boundary shared by the CLI update application. */
401
401
  export function createSelfUpdateQuiescePort(root) {
402
402
  return {
403
+ read: () => root.maintenance.read(),
403
404
  acquire: (tag, retryFailed) => root.maintenance.acquire(tag, retryFailed),
404
405
  exclusive: (tag, retryFailed, operation) => root.maintenance.runExclusive(tag, retryFailed, operation),
405
406
  activeRuns: () => activeExecutionRuns(root),
@@ -433,18 +434,20 @@ function createSandboxRuntimeApplications(root) {
433
434
  };
434
435
  }
435
436
  /**
436
- * Builds a running self-update on the sandbox container onto a specific
437
- * image tag: rebuild the image, swap the container onto it, and confirm the
438
- * resident `wake start` process actually came back up. Reused for both the
439
- * forward deploy and a same-shaped rollback to a previously-built tag.
437
+ * Builds a candidate image before the maintenance window. A failed build
438
+ * cannot affect the running sandbox.
440
439
  */
441
- async function deploySandboxTag(root, docker, tag, image, overrides) {
440
+ async function buildSandboxTag(root, docker, image, overrides) {
442
441
  const port = createSandboxDockerPort(docker, sandboxDockerOptions(root, { image, ...overrides }));
443
- const wakeInvocation = overrides?.development?.mode === 'source'
442
+ await port.build();
443
+ }
444
+ /** Swaps the already-built candidate image and verifies the resident host. */
445
+ async function deploySandboxTag(root, docker, tag, image, development) {
446
+ const port = createSandboxDockerPort(docker, sandboxDockerOptions(root, { image, development }));
447
+ const wakeInvocation = development?.mode === 'source'
444
448
  ? ['node', '/app/dist/src/main.js']
445
449
  : sandboxWakeInvocation(root);
446
450
  const healthcheckRoot = `/tmp/wake-self-update-healthcheck-${tag}`;
447
- await port.build();
448
451
  try {
449
452
  await port.update();
450
453
  }
@@ -541,15 +544,14 @@ function createOperationalApplications(root) {
541
544
  ledger: createUpdateLedger(root.paths.wakeRoot),
542
545
  source: updatePort,
543
546
  rollout: {
544
- async deploy(deployTag) {
545
- await waitForActiveRuns({
546
- activeRunIds: () => activeExecutionRunIds(root),
547
- sleep: async (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
548
- });
549
- await deploySandboxTag(root, dockerCli, deployTag, `${imageRepository}:${deployTag}`, {
547
+ async prepare(prepareTag) {
548
+ await buildSandboxTag(root, dockerCli, `${imageRepository}:${prepareTag}`, {
550
549
  development: updateDevelopment,
551
- buildVersion: deployTag,
550
+ buildVersion: prepareTag,
552
551
  });
552
+ },
553
+ async deploy(deployTag) {
554
+ await deploySandboxTag(root, dockerCli, deployTag, `${imageRepository}:${deployTag}`, updateDevelopment);
553
555
  // A later deploy succeeding after a prior rollback clears the stale warning.
554
556
  await failureLog.clear();
555
557
  },
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
124
124
  return `g${headHash.slice(0, 7)}`;
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "v0.4.43";
127
+ export const wakeVersion = "v0.4.44";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.4.43",
3
+ "version": "0.4.44",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {