@atolis-hq/wake 0.3.84 → 0.3.85

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.
@@ -1,5 +1,13 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
+ // recordUpdateFailure (self-update-application.ts) records every failed
4
+ // attempt through this same log regardless of stage — including one that
5
+ // never reached rollout.deploy() at all, e.g. a quiesce timeout waiting on
6
+ // active Runs. "rolled back" would misreport those as a completed deploy
7
+ // that had to be undone, when nothing was ever deployed.
8
+ export function describeSelfUpdateFailure(failure) {
9
+ return `update to ${failure.tag} failed at ${failure.occurredAt}: ${failure.message}`;
10
+ }
3
11
  /**
4
12
  * Persists the most recent self-update rollback so it can surface on the
5
13
  * operator health screen instead of relying on a third-party notification —
@@ -3,7 +3,7 @@ import { ApiCommandStatus, fromWorkItemKey, presentBoardCard, presentResource, p
3
3
  import { analyticsProjection } from './analytics-projection.js';
4
4
  import { boardConditionCounts, boardProjection, } from './board-projection.js';
5
5
  import { primaryExternalRef } from './external-ref.js';
6
- import { createSelfUpdateFailureLog } from './self-update-failure-log.js';
6
+ import { createSelfUpdateFailureLog, describeSelfUpdateFailure, } from './self-update-failure-log.js';
7
7
  import { createExecutionApplications } from './surface-api-execution-applications.js';
8
8
  import { projectionMeta, sampledMeta } from './surface-api-metadata.js';
9
9
  import { projectionPage } from './surface-api-projection-pages.js';
@@ -200,7 +200,7 @@ function createSystemApplications(root, now) {
200
200
  : {
201
201
  name: 'self-update',
202
202
  status: 'degraded',
203
- detail: `rolled back from ${selfUpdateFailure.tag} at ${selfUpdateFailure.occurredAt}: ${selfUpdateFailure.message}`,
203
+ detail: describeSelfUpdateFailure(selfUpdateFailure),
204
204
  },
205
205
  ];
206
206
  const adapters = root.providers.flatMap((instance) => (instance.health?.() ?? []).map((check) => ({
@@ -309,15 +309,25 @@ async function performTick(root, now, command, sequence) {
309
309
  result: await readControlPlaneStatus(root, now),
310
310
  };
311
311
  }
312
- async function readControlPlaneStatus(root, now) {
312
+ export async function readControlPlaneStatus(root, now) {
313
313
  const stored = await root.projections.read(ControlStreamKind.Global, 'global');
314
314
  const meta = await projectionMeta(root.journal, stored === null ? [] : [stored], now());
315
+ const lease = await root.maintenance.read();
315
316
  return {
316
317
  data: {
317
318
  paused: stored?.value.pausedUntil !== null && stored?.value.pausedUntil !== undefined,
318
319
  ...(stored?.value.pausedUntil == null ? {} : { pausedUntil: stored.value.pausedUntil }),
319
320
  ...(stored?.value.reason === undefined ? {} : { reason: stored.value.reason }),
320
321
  updatedAt: meta.asOf,
322
+ ...(lease === null
323
+ ? {}
324
+ : {
325
+ maintenanceLease: {
326
+ phase: lease.phase,
327
+ startedAt: lease.startedAt,
328
+ ...(lease.failure === undefined ? {} : { failure: lease.failure }),
329
+ },
330
+ }),
321
331
  },
322
332
  meta,
323
333
  };
@@ -9,7 +9,7 @@ import { IntakeHost, ResidentHost, TickHost } from '../control-plane/index.js';
9
9
  import { ExecutionCancellationReason, ExecutionFailureCode, RunStatus, loadPromptTemplate, } from '../execution/index.js';
10
10
  import { EventActorKind, correlationId } from '../kernel/index.js';
11
11
  import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
12
- import { DockerProcessError, createApiDispatcher, createApiHttpServer, createLoggedDockerCli, createPackagedAssetSource, createProcessLogSink, createSandboxDockerPort, drainProcessOutput, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, } from '../surfaces/index.js';
12
+ import { DockerProcessError, createApiDispatcher, createApiHttpServer, createLoggedDockerCli, createPackagedAssetSource, createProcessLogSink, createSandboxDockerPort, drainProcessOutput, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
13
13
  import { WorkStreamKind, workItemId } from '../work/index.js';
14
14
  import { loadConfig } from './config/load-config.js';
15
15
  import { runtimeProjectionDefinitions } from './projection-runtime.js';
@@ -456,6 +456,7 @@ async function doctorDiagnostics(root) {
456
456
  }
457
457
  }
458
458
  await checkProviders(root, failures, notices);
459
+ await checkMaintenanceLease(root, failures);
459
460
  notices.push(...(await dockerSandboxHealthNotices(root)));
460
461
  for (const [name, path] of Object.entries({
461
462
  events: root.paths.eventsRoot,
@@ -490,6 +491,21 @@ function referencedPromptTemplateNames(workflows) {
490
491
  }
491
492
  return [...names];
492
493
  }
494
+ // A retained maintenance lease pauses every resident loop (intake and
495
+ // dispatch) regardless of phase -- including Failed, since it's the
496
+ // operator's decision whether a failed attempt is safe to retry or clear.
497
+ // Without this check the pause is invisible: the process ticks normally and
498
+ // logs nothing, so a stuck lease from an update that couldn't quiesce active
499
+ // Runs looks identical to a healthy idle system.
500
+ async function checkMaintenanceLease(root, failures) {
501
+ const lease = await root.maintenance.read();
502
+ if (lease === null)
503
+ return;
504
+ const detail = lease.failure === undefined ? '' : ` (${lease.failure})`;
505
+ failures.push(`update maintenance lease is held in phase "${lease.phase}" since ${lease.startedAt}${detail} -- ` +
506
+ 'every resident loop stays paused until it is cleared or resumes; run `wake self-update` ' +
507
+ 'to retry, or clear the lease manually if the attempt is abandoned');
508
+ }
493
509
  async function checkProviders(root, failures, notices) {
494
510
  for (const provider of root.providers) {
495
511
  if (provider.adapter.trim().length === 0) {
@@ -661,7 +677,7 @@ function createSandboxEntrypointDependencies(root) {
661
677
  waitForExit: async (pid) => children.get(pid) ?? 1,
662
678
  writeFile: (path, content) => writeFileContent(path, content, 'utf8'),
663
679
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
664
- waitForever: () => new Promise(() => { }),
680
+ waitForever,
665
681
  log: (message) => process.stdout.write(`${message}\n`),
666
682
  };
667
683
  }
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "g80c78a2";
111
+ export const wakeVersion = "g6de20bb";
@@ -15,6 +15,15 @@ export class FileEventJournal {
15
15
  return this.changeSignalSource;
16
16
  }
17
17
  cached;
18
+ // Every read (readStream/readAll/readLatest/latestGlobalPosition) reaches
19
+ // scan(), and the resident host's tick loop calls those many times a
20
+ // second while work is progressing. Without coalescing, calls that land
21
+ // concurrently each independently decide the cache is stale and each
22
+ // re-read and re-parse the entire on-disk journal — under I/O pressure
23
+ // (e.g. a concurrently running build/test process competing for disk),
24
+ // enough of these can overlap at once to exhaust the heap. Sharing one
25
+ // in-flight decode among concurrent callers makes that impossible.
26
+ inFlightScan;
18
27
  async append(stream, expectedSequence, drafts) {
19
28
  return withFileLock(join(this.root, 'locks', 'event-journal.lock'), async () => {
20
29
  const current = await this.scan();
@@ -97,7 +106,17 @@ export class FileEventJournal {
97
106
  : [...priorEntries, updatedEntry];
98
107
  this.cached = { entries, events: [...priorEvents, ...newEnvelopes] };
99
108
  }
100
- async scan() {
109
+ scan() {
110
+ if (this.inFlightScan !== undefined)
111
+ return this.inFlightScan;
112
+ const run = this.scanUncoalesced().finally(() => {
113
+ if (this.inFlightScan === run)
114
+ this.inFlightScan = undefined;
115
+ });
116
+ this.inFlightScan = run;
117
+ return run;
118
+ }
119
+ async scanUncoalesced() {
101
120
  const directory = join(this.root, 'events');
102
121
  let files;
103
122
  try {
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Never resolves, and never lets the process exit either. The supervised
3
+ * child is spawned detached and unref'd (see spawnDetached), so an
4
+ * unresolved Promise alone is not enough here — a Promise executor registers
5
+ * no libuv handle, and once other pending I/O quiets down Node treats the
6
+ * still-pending top-level await as unsettled and exits anyway. A ref'd
7
+ * timer is a real handle, so it keeps the process alive for as long as this
8
+ * Promise is meant to.
9
+ */
10
+ export function waitForever() {
11
+ return new Promise(() => {
12
+ setInterval(() => { }, 1 << 30);
13
+ });
14
+ }
1
15
  /**
2
16
  * Restarts `wake start` on every exit — a first-boot crash (e.g. missing
3
17
  * sandbox auth) must not stop the retry loop, since the operator's only path