@atolis-hq/wake 0.4.32 → 0.4.34

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,11 +1,13 @@
1
1
  import { type ApiApplications } from '../surfaces/index.js';
2
2
  import type { CompositionRoot } from './composition-root.js';
3
+ import { UpdateMaintenancePhase } from './update-maintenance-lease.js';
3
4
  export declare function createSurfaceApiApplications(root: CompositionRoot, now: () => string): ApiApplications;
4
5
  export declare function readControlPlaneStatus(root: CompositionRoot, now: () => string): Promise<{
5
6
  data: {
6
7
  maintenanceLease?: {
7
8
  failure?: string;
8
- phase: import("./update-maintenance-lease.js").UpdateMaintenancePhase;
9
+ attemptId: string;
10
+ phase: UpdateMaintenancePhase;
9
11
  startedAt: string;
10
12
  };
11
13
  updatedAt: string;
@@ -9,6 +9,7 @@ import { projectionMeta, sampledMeta } from './surface-api-metadata.js';
9
9
  import { projectionPage } from './surface-api-projection-pages.js';
10
10
  import { createSurfaceWorkApplications } from './surface-api-work-applications.js';
11
11
  import { createWorkflowDiagramApplications } from './surface-api-workflow-diagrams.js';
12
+ import { UpdateMaintenancePhase } from './update-maintenance-lease.js';
12
13
  import { wakeVersion } from './version.js';
13
14
  export function createSurfaceApiApplications(root, now) {
14
15
  return {
@@ -366,6 +367,33 @@ function createControlPlaneApplications(root, now) {
366
367
  await root.controlPlane.resume(command.idempotencyKey);
367
368
  return commandAccepted(command, now());
368
369
  },
370
+ async clearMaintenance(command) {
371
+ const lease = await root.maintenance.read();
372
+ if (lease === null ||
373
+ lease.attemptId !== command.attemptId ||
374
+ lease.phase !== UpdateMaintenancePhase.Failed)
375
+ return maintenanceClearConflict(lease);
376
+ try {
377
+ await root.maintenance.clearFailed(command.attemptId);
378
+ }
379
+ catch {
380
+ return maintenanceClearConflict(await root.maintenance.read());
381
+ }
382
+ return commandAccepted(command, now());
383
+ },
384
+ };
385
+ }
386
+ function maintenanceClearConflict(lease) {
387
+ return {
388
+ conflict: true,
389
+ code: 'maintenance-not-clearable',
390
+ detail: 'The failed maintenance lease is no longer available to clear',
391
+ retryable: false,
392
+ ...(lease === null
393
+ ? {}
394
+ : {
395
+ current: { attemptId: lease.attemptId, phase: lease.phase, startedAt: lease.startedAt },
396
+ }),
369
397
  };
370
398
  }
371
399
  export async function readControlPlaneStatus(root, now) {
@@ -382,6 +410,7 @@ export async function readControlPlaneStatus(root, now) {
382
410
  ? {}
383
411
  : {
384
412
  maintenanceLease: {
413
+ attemptId: lease.attemptId,
385
414
  phase: lease.phase,
386
415
  startedAt: lease.startedAt,
387
416
  ...(lease.failure === undefined ? {} : { failure: lease.failure }),
@@ -9,7 +9,7 @@ import { BuiltInActivityName, agentActivityDefinition } from '../activities/inde
9
9
  import { IntakeHost, ResidentHost, TickHost, } from '../control-plane/index.js';
10
10
  import { ExecutionFailureCode, RunStatus, isActiveRunStatus, loadPromptTemplate, } from '../execution/index.js';
11
11
  import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
12
- import { DockerProcessError, createApiDispatcher, createLoggedDockerCli, createPackagedAssetSource, createPairingGrant, createProcessLogSink, createSandboxDockerPort, createSurfaceHttpServer, drainProcessOutput, loadOrCreateCredentials, promoteSandboxImage, redeemPairingGrant, replaceAccessKey, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
12
+ import { DockerProcessError, createApiDispatcher, createLoggedDockerCli, createPackagedAssetSource, createPairingGrant, createProcessLogSink, createSandboxDockerPort, createSurfaceHttpServer, describeSandboxStartupFailure, drainProcessOutput, loadOrCreateCredentials, promoteSandboxImage, redeemPairingGrant, replaceAccessKey, 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 { createNpmUpdatePort } from './npm-update-port.js';
@@ -427,7 +427,12 @@ async function deploySandboxTag(root, docker, tag, image, overrides) {
427
427
  : sandboxWakeInvocation(root);
428
428
  const healthcheckRoot = `/tmp/wake-self-update-healthcheck-${tag}`;
429
429
  await port.build();
430
- await port.update();
430
+ try {
431
+ await port.update();
432
+ }
433
+ catch (error) {
434
+ throw await describeSandboxStartupFailure(docker, root.config.host.sandbox.containerName, error);
435
+ }
431
436
  await verifyResidentStart(docker, root.config.host.sandbox.containerName, [...wakeInvocation, 'start', '--wake-root', '/wake'].join(' '));
432
437
  await port.exec([...wakeInvocation, 'init', healthcheckRoot]);
433
438
  await port.exec([...wakeInvocation, 'tick', '--wake-root', healthcheckRoot, '--no-sandbox']);
@@ -456,6 +461,16 @@ function createOperationalApplications(root) {
456
461
  },
457
462
  },
458
463
  }),
464
+ maintenance: async (arguments_) => {
465
+ const command = withoutOperationalWakeRoot(arguments_);
466
+ if (command.length !== 2 || command[0] !== 'clear' || command[1] !== '--failed')
467
+ throw new Error('wake maintenance clear requires --failed');
468
+ const lease = await root.maintenance.read();
469
+ if (lease === null)
470
+ return { cleared: false };
471
+ await root.maintenance.clearFailed(lease.attemptId);
472
+ return { cleared: true, tag: lease.tag };
473
+ },
459
474
  sandboxSetup: async (arguments_) => {
460
475
  if (arguments_.length > 0)
461
476
  throw new Error('wake sandbox-setup accepts no arguments');
@@ -766,6 +781,19 @@ function selfUpdateLoopInterval(arguments_) {
766
781
  throw new Error('wake self-update --loop-interval-ms must be a positive integer');
767
782
  return interval;
768
783
  }
784
+ function withoutOperationalWakeRoot(arguments_) {
785
+ const result = [];
786
+ for (let index = 0; index < arguments_.length; index += 1) {
787
+ if (arguments_[index] !== '--wake-root') {
788
+ result.push(arguments_[index]);
789
+ continue;
790
+ }
791
+ if (arguments_[index + 1] === undefined)
792
+ throw new Error('wake maintenance requires --wake-root <path>');
793
+ index += 1;
794
+ }
795
+ return result;
796
+ }
769
797
  function delay(milliseconds) {
770
798
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
771
799
  }
@@ -20,5 +20,6 @@ export interface UpdateMaintenanceLease {
20
20
  transition(phase: Exclude<UpdateMaintenancePhase, typeof UpdateMaintenancePhase.Failed>, attemptId?: string): Promise<UpdateMaintenanceState>;
21
21
  fail(error: unknown, attemptId?: string): Promise<UpdateMaintenanceState>;
22
22
  clear(attemptId?: string): Promise<void>;
23
+ clearFailed(attemptId: string): Promise<void>;
23
24
  }
24
25
  export declare function createUpdateMaintenanceLease(wakeRoot: string, now?: () => string, createAttemptId?: () => string, isProcessAlive?: (pid: number) => boolean): UpdateMaintenanceLease;
@@ -72,6 +72,15 @@ export function createUpdateMaintenanceLease(wakeRoot, now = () => new Date().to
72
72
  await rm(path, { force: true });
73
73
  });
74
74
  },
75
+ async clearFailed(attemptId) {
76
+ await withLeaseLock(path, async () => {
77
+ const current = await requireState(path);
78
+ requireOwnership(current, attemptId);
79
+ if (current.phase !== UpdateMaintenancePhase.Failed)
80
+ throw new Error('Only a failed maintenance lease can be cleared by an operator');
81
+ await rm(path, { force: true });
82
+ });
83
+ },
75
84
  };
76
85
  }
77
86
  function requireOwnership(state, attemptId) {
@@ -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.32";
127
+ export const wakeVersion = "v0.4.34";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -1,5 +1,4 @@
1
1
  import { ProviderQuotaExceededFailureKind } from '../../contracts/runner.js';
2
- import { WorkspaceMode } from '../../contracts/vocabulary.js';
3
2
  import { cliRunner } from './claude.js';
4
3
  // Deliberately narrow: only genuine provider usage/rate-limit phrasing.
5
4
  // Auth/login failures (unauthorized, authentication, api key, not logged
@@ -67,8 +66,10 @@ export function codexCommandArgs(request, passthroughArgs = [], defaults = {}) {
67
66
  }
68
67
  // Work around Codex's missing background-command lifecycle contract (https://github.com/openai/codex/issues/32505, https://github.com/openai/codex/issues/34866).
69
68
  const waitBackgroundInstruction = `When a command you started is still running or lacks a terminal result, do not report a normal Wake outcome. Make your entire final response exactly WAIT_BACKGROUND. Wake will reject that stop attempt and ask you to continue. After that hook prompt, poll the still-running command and report a normal terminal status only after it has settled. Do not use WAIT_BACKGROUND for a human-input or implementation blocker.`;
70
- function codexSandboxMode(workspaceMode) {
71
- return workspaceMode === WorkspaceMode.Branch ? 'danger-full-access' : 'workspace-write';
69
+ function codexSandboxMode(_workspaceMode) {
70
+ // Wake executes runners inside its Docker sandbox. Avoid asking Codex to
71
+ // create a second Linux namespace sandbox, which container runtimes may deny.
72
+ return 'danger-full-access';
72
73
  }
73
74
  export function parseCodexOutput(stdout, request) {
74
75
  let state = {};
@@ -11,6 +11,7 @@ export interface ControlPlaneStatusResponse {
11
11
  * lease exists, including a failed one -- see update-maintenance-lease.ts.
12
12
  */
13
13
  readonly maintenanceLease?: {
14
+ readonly attemptId: string;
14
15
  readonly phase: string;
15
16
  readonly startedAt: string;
16
17
  readonly failure?: string;
@@ -20,6 +20,9 @@ export type ApiResourceResult<T> = ResourceResponse<T>;
20
20
  export interface ApiCommandRequest {
21
21
  readonly idempotencyKey: string;
22
22
  }
23
+ export interface ApiClearMaintenanceRequest extends ApiCommandRequest {
24
+ readonly attemptId: string;
25
+ }
23
26
  export type ApiRunResolutionRequest = (ApiCommandRequest & {
24
27
  readonly status: typeof RunStatus.Succeeded;
25
28
  readonly outcome: unknown;
@@ -46,6 +49,7 @@ export interface ApiApplications {
46
49
  status(): Promise<ApiResourceResult<ControlPlaneStatusResponse>>;
47
50
  pause?(command: ApiCommandRequest): Promise<ApiCommandResult>;
48
51
  resume?(command: ApiCommandRequest): Promise<ApiCommandResult>;
52
+ clearMaintenance?(command: ApiClearMaintenanceRequest): Promise<ApiCommandResult>;
49
53
  };
50
54
  readonly work: {
51
55
  list(query: CollectionQuery): Promise<ApiCollectionPage<WorkItemResponse>>;
@@ -10,6 +10,9 @@ export async function dispatchCommand(applications, url, body) {
10
10
  const work = await dispatchWorkCommand(applications, url.pathname, body);
11
11
  if (work !== undefined)
12
12
  return work;
13
+ const maintenance = await dispatchMaintenanceClear(applications, url.pathname, body);
14
+ if (maintenance !== undefined)
15
+ return maintenance;
13
16
  const request = commandRequest(body);
14
17
  if (isHttpResponse(request))
15
18
  return request;
@@ -78,6 +81,8 @@ async function dispatchControlCommand(applications, pathname, request) {
78
81
  const name = controlCommandName(pathname);
79
82
  if (name === undefined)
80
83
  return undefined;
84
+ if (name === 'clear-maintenance')
85
+ return undefined;
81
86
  const status = await applications.controlPlane.status();
82
87
  const operation = applications.controlPlane[name];
83
88
  return operation === undefined
@@ -89,7 +94,33 @@ function controlCommandName(pathname) {
89
94
  if (!pathname.startsWith(prefix))
90
95
  return undefined;
91
96
  const name = pathname.slice(prefix.length);
92
- return name === 'pause' || name === 'resume' ? name : undefined;
97
+ return name === 'pause' || name === 'resume' || name === 'clear-maintenance' ? name : undefined;
98
+ }
99
+ async function dispatchMaintenanceClear(applications, pathname, body) {
100
+ if (pathname !== '/api/v1/control-plane/commands/clear-maintenance')
101
+ return undefined;
102
+ const request = clearMaintenanceRequest(body);
103
+ if (isHttpResponse(request))
104
+ return request;
105
+ const operation = applications.controlPlane.clearMaintenance;
106
+ const status = await applications.controlPlane.status();
107
+ return operation === undefined
108
+ ? unavailable('clear-maintenance', status.data)
109
+ : accepted(await operation(request), applications.now());
110
+ }
111
+ function clearMaintenanceRequest(body) {
112
+ if (!isObject(body))
113
+ return invalidRequest('idempotencyKey', 'A JSON object with an idempotency key is required');
114
+ if (Object.keys(body).some((key) => key !== 'idempotencyKey' && key !== 'attemptId'))
115
+ return invalidRequest('', 'The command body contains unknown fields');
116
+ const command = commandRequest({ idempotencyKey: body.idempotencyKey });
117
+ if (isHttpResponse(command))
118
+ return command;
119
+ if (typeof body.attemptId !== 'string' || body.attemptId.trim() === '')
120
+ return invalidRequest('attemptId', 'Must be a non-empty string');
121
+ if (body.attemptId.length > 200)
122
+ return invalidRequest('attemptId', 'Must be at most 200 characters');
123
+ return { ...command, attemptId: body.attemptId };
93
124
  }
94
125
  async function dispatchRunnerCommand(applications, pathname, request) {
95
126
  const match = /^\/api\/v1\/runners\/([^/]+)\/commands\/(pause|unpause)$/.exec(pathname);
@@ -1 +1 @@
1
- export declare const controlPlaneRoutes: readonly ["/api/v1/control-plane/status", "/api/v1/control-plane/commands/pause", "/api/v1/control-plane/commands/resume"];
1
+ export declare const controlPlaneRoutes: readonly ["/api/v1/control-plane/status", "/api/v1/control-plane/commands/pause", "/api/v1/control-plane/commands/resume", "/api/v1/control-plane/commands/clear-maintenance"];
@@ -2,4 +2,5 @@ export const controlPlaneRoutes = [
2
2
  '/api/v1/control-plane/status',
3
3
  '/api/v1/control-plane/commands/pause',
4
4
  '/api/v1/control-plane/commands/resume',
5
+ '/api/v1/control-plane/commands/clear-maintenance',
5
6
  ];
@@ -1,4 +1,4 @@
1
1
  import type { ApiDispatcher } from '../http-server.js';
2
2
  import type { ApiApplications } from './applications.js';
3
- export type { ApiApplications, ApiCollectionPage, ApiCommandRequest, ApiResourceResult, ApiRunResolutionRequest, ApiSystemApplications, CollectionQuery, } from './applications.js';
3
+ export type { ApiApplications, ApiClearMaintenanceRequest, ApiCollectionPage, ApiCommandRequest, ApiResourceResult, ApiRunResolutionRequest, ApiSystemApplications, CollectionQuery, } from './applications.js';
4
4
  export declare function createApiDispatcher(applications: ApiApplications): ApiDispatcher;
@@ -0,0 +1,7 @@
1
+ import { type DockerCli } from './docker-cli.js';
2
+ /**
3
+ * Adds the short-lived replacement container's logs to a rollout failure
4
+ * before rollback removes that container. Docker otherwise reports only the
5
+ * follow-on `exec` failure, which hides the actual entrypoint error.
6
+ */
7
+ export declare function describeSandboxStartupFailure(docker: DockerCli, containerName: string, error: unknown): Promise<Error>;
@@ -0,0 +1,33 @@
1
+ import { DockerProcessError } from './docker-cli.js';
2
+ import { scrubProcessLog } from './process-log.js';
3
+ /**
4
+ * Adds the short-lived replacement container's logs to a rollout failure
5
+ * before rollback removes that container. Docker otherwise reports only the
6
+ * follow-on `exec` failure, which hides the actual entrypoint error.
7
+ */
8
+ export async function describeSandboxStartupFailure(docker, containerName, error) {
9
+ const failure = formatDockerError(error);
10
+ try {
11
+ const output = await docker.invoke(['logs', '--tail', '100', containerName], {
12
+ suppressOutput: true,
13
+ });
14
+ const logs = scrubProcessLog(`${output.stdout}${output.stderr}`).trim();
15
+ if (logs.length > 0)
16
+ return new Error(`Sandbox replacement "${containerName}" failed during startup: ${failure}\n` +
17
+ `Container logs:\n${logs}`, { cause: error });
18
+ }
19
+ catch (logsError) {
20
+ return new Error(`Sandbox replacement "${containerName}" failed during startup: ${failure}. ` +
21
+ `Could not collect its logs: ${formatDockerError(logsError)}`, { cause: error });
22
+ }
23
+ return new Error(`Sandbox replacement "${containerName}" failed during startup: ${failure}`, {
24
+ cause: error,
25
+ });
26
+ }
27
+ function formatDockerError(error) {
28
+ if (error instanceof DockerProcessError) {
29
+ const output = `${error.result.stdout}${error.result.stderr}`.trim();
30
+ return output.length === 0 ? error.message : `${error.message}: ${scrubProcessLog(output)}`;
31
+ }
32
+ return error instanceof Error ? error.message : String(error);
33
+ }
@@ -5,6 +5,9 @@ export interface HostOptions {
5
5
  readonly port?: number;
6
6
  readonly wakeRoot?: string;
7
7
  }
8
+ export declare const WakeOperationalCommand: {
9
+ readonly Maintenance: "maintenance";
10
+ };
8
11
  export type WakeCommand = ({
9
12
  readonly kind: 'tick' | 'start' | 'stop';
10
13
  } & {
@@ -37,7 +40,7 @@ export type WakeCommand = ({
37
40
  readonly rebuildProjections: boolean;
38
41
  readonly wakeRoot?: string;
39
42
  } | {
40
- readonly kind: 'init' | 'doctor' | 'sandbox-setup' | 'sandbox-entrypoint' | 'self-update' | 'smoke';
43
+ readonly kind: 'init' | 'doctor' | typeof WakeOperationalCommand.Maintenance | 'sandbox-setup' | 'sandbox-entrypoint' | 'self-update' | 'smoke';
41
44
  readonly arguments: readonly string[];
42
45
  } | {
43
46
  readonly kind: 'sandbox';
@@ -59,6 +62,7 @@ export interface WakeCliApplications {
59
62
  readonly operational?: {
60
63
  readonly init: (arguments_: readonly string[]) => Promise<unknown>;
61
64
  readonly doctor: (arguments_: readonly string[]) => Promise<unknown>;
65
+ readonly maintenance: (arguments_: readonly string[]) => Promise<unknown>;
62
66
  readonly sandbox: (arguments_: readonly string[]) => Promise<unknown>;
63
67
  readonly sandboxSetup: (arguments_: readonly string[]) => Promise<unknown>;
64
68
  readonly sandboxEntrypoint: (arguments_: readonly string[]) => Promise<unknown>;
@@ -1,6 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import qrcodeTerminal from 'qrcode-terminal';
3
3
  import { ExecutionStreamKind, RunStatus } from '../../execution/index.js';
4
+ import { defineClosedVocabulary } from '../../kernel/index.js';
5
+ export const WakeOperationalCommand = defineClosedVocabulary({
6
+ Maintenance: 'maintenance',
7
+ });
4
8
  const defaultBudget = { maxAdvances: 100, maxRuns: 100, maxDurationMs: 30_000 };
5
9
  /** Parses Surface vocabulary only; Bootstrap supplies every application facade. */
6
10
  // eslint-disable-next-line complexity -- command vocabulary is intentionally exhaustive at the Surface boundary.
@@ -29,6 +33,7 @@ export function parseWakeCommand(arguments_) {
29
33
  return parseResidentCommand(command, arguments_.slice(1));
30
34
  case 'init':
31
35
  case 'doctor':
36
+ case WakeOperationalCommand.Maintenance:
32
37
  case 'sandbox-setup':
33
38
  case 'sandbox-entrypoint':
34
39
  case 'sandbox':
@@ -153,6 +158,9 @@ export async function runWakeCommand(command, applications, output, signal) {
153
158
  case 'doctor':
154
159
  writeResult(output, await operational(applications).doctor(command.arguments));
155
160
  return;
161
+ case WakeOperationalCommand.Maintenance:
162
+ writeResult(output, await operational(applications).maintenance(command.arguments));
163
+ return;
156
164
  case 'sandbox':
157
165
  writeResult(output, await operational(applications).sandbox(command.arguments));
158
166
  return;
@@ -16,6 +16,7 @@ export const usage = [
16
16
  ' wake correlate Manually correlate a resource to a work item',
17
17
  ' wake run resolve Resolve an escalated ambiguous run',
18
18
  ' wake doctor Diagnose config/GitHub/Docker/sandbox setup problems',
19
+ ' wake maintenance clear Clear an abandoned failed self-update maintenance lease',
19
20
  ' wake --version Print the installed Wake version',
20
21
  ' wake --help Show this message',
21
22
  '',
@@ -25,6 +26,7 @@ export const usage = [
25
26
  ' wake api Run the target API surface',
26
27
  ' wake sandbox-entrypoint Run the sandbox resident entrypoint',
27
28
  ' wake self-update Safely update Wake from npm or a source checkout',
29
+ ' wake maintenance clear --failed',
28
30
  '',
29
31
  'Getting started:',
30
32
  ' 1. wake init ./wake-home',
@@ -42,6 +42,7 @@ export * from './cli/commands/ui.js';
42
42
  export * from './cli/commands/validate-state.js';
43
43
  export * from './cli/infrastructure/docker-cli.js';
44
44
  export * from './cli/infrastructure/process-log.js';
45
+ export * from './cli/infrastructure/sandbox-startup-failure.js';
45
46
  export * from './cli/main.js';
46
47
  export * from './cli/usage.js';
47
48
  export * from './contracts/config.js';
@@ -42,6 +42,7 @@ export * from './cli/commands/ui.js';
42
42
  export * from './cli/commands/validate-state.js';
43
43
  export * from './cli/infrastructure/docker-cli.js';
44
44
  export * from './cli/infrastructure/process-log.js';
45
+ export * from './cli/infrastructure/sandbox-startup-failure.js';
45
46
  export * from './cli/main.js';
46
47
  export * from './cli/usage.js';
47
48
  export * from './contracts/config.js';