@atolis-hq/wake 0.3.32 → 0.3.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.
- package/dist/src/bootstrap/surface-cli-applications.js +17 -12
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/delivery/application/delivery-outcome-reactor.js +31 -37
- package/dist/src/surfaces/cli/infrastructure/docker-cli.js +34 -32
- package/dist/src/surfaces/cli/infrastructure/docker-invocation.js +9 -0
- package/dist/src/surfaces/cli/infrastructure/resident-start.js +42 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import { IntakeHost, ResidentHost, TickHost } from '../control-plane/index.js';
|
|
|
9
9
|
import { ExecutionCancellationReason, 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 { 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, } 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';
|
|
@@ -244,7 +244,9 @@ function createSandboxRuntimeApplications(root) {
|
|
|
244
244
|
return false;
|
|
245
245
|
}
|
|
246
246
|
},
|
|
247
|
-
exec
|
|
247
|
+
async exec(arguments_) {
|
|
248
|
+
await docker.exec([...wakeInvocation, ...arguments_]);
|
|
249
|
+
},
|
|
248
250
|
};
|
|
249
251
|
}
|
|
250
252
|
/**
|
|
@@ -371,32 +373,35 @@ function spawnDocker(arguments_, onChunk, cwd, options) {
|
|
|
371
373
|
child.on('error', reject);
|
|
372
374
|
child.on('close', (code) => {
|
|
373
375
|
if (code === 0)
|
|
374
|
-
resolve();
|
|
376
|
+
resolve({ stdout: '', stderr: '' });
|
|
375
377
|
else
|
|
376
|
-
reject(new
|
|
378
|
+
reject(new DockerProcessError('docker ' + arguments_.join(' ') + ' exited with code ' + String(code), { stdout: '', stderr: '' }));
|
|
377
379
|
});
|
|
378
380
|
return;
|
|
379
381
|
}
|
|
380
382
|
let chain = Promise.resolve();
|
|
381
|
-
let
|
|
383
|
+
let stdout = '';
|
|
384
|
+
let stderr = '';
|
|
382
385
|
const enqueue = (stream, text) => {
|
|
383
|
-
if (stream === '
|
|
384
|
-
|
|
386
|
+
if (stream === 'stdout')
|
|
387
|
+
stdout += text;
|
|
388
|
+
else
|
|
389
|
+
stderr += text;
|
|
385
390
|
chain = chain.then(() => onChunk({ stream, text }));
|
|
386
391
|
};
|
|
387
392
|
child.stdout.on('data', (buffer) => enqueue('stdout', buffer.toString('utf8')));
|
|
388
393
|
child.stderr.on('data', (buffer) => enqueue('stderr', buffer.toString('utf8')));
|
|
389
394
|
child.on('error', (error) => {
|
|
390
|
-
void chain.then(() => reject(error));
|
|
395
|
+
void chain.then(() => reject(new DockerProcessError(error.message, { stdout, stderr }, error)));
|
|
391
396
|
});
|
|
392
397
|
child.on('close', (code) => {
|
|
393
398
|
void chain.then(() => {
|
|
394
399
|
if (code === 0)
|
|
395
|
-
resolve();
|
|
400
|
+
resolve({ stdout, stderr });
|
|
396
401
|
else {
|
|
397
|
-
const detail =
|
|
398
|
-
reject(new
|
|
399
|
-
(detail.length > 0 ? `: ${detail}` : '')));
|
|
402
|
+
const detail = stderr.trim();
|
|
403
|
+
reject(new DockerProcessError(`docker ${arguments_.join(' ')} exited with code ${String(code)}` +
|
|
404
|
+
(detail.length > 0 ? `: ${detail}` : ''), { stdout, stderr }));
|
|
400
405
|
}
|
|
401
406
|
});
|
|
402
407
|
});
|
|
@@ -16,48 +16,42 @@ export class DeliveryOutcomeReactor {
|
|
|
16
16
|
async runOnce() {
|
|
17
17
|
const consumer = 'reactor:delivery-outcomes';
|
|
18
18
|
const events = await this.journal.readAll(await this.checkpoints.load(consumer));
|
|
19
|
+
const resolvedDeliveryEventIds = new Set();
|
|
19
20
|
for (const event of events) {
|
|
20
|
-
|
|
21
|
-
if (delivery !== null) {
|
|
22
|
-
const command = {
|
|
23
|
-
workflowInstanceId: workflowInstanceId(delivery.payload.workflowInstanceId),
|
|
24
|
-
activationId: activationId(delivery.payload.activationId),
|
|
25
|
-
};
|
|
26
|
-
if (await this.isAwaitingThisDelivery(command, delivery.payload.intentEventId)) {
|
|
27
|
-
if (delivery.eventType === DeliveryEventType.Confirmed ||
|
|
28
|
-
(delivery.eventType === DeliveryEventType.Reconciled &&
|
|
29
|
-
delivery.payload.result === DeliveryResultKind.Confirmed))
|
|
30
|
-
await this.orchestration.acceptOutcome({
|
|
31
|
-
...command,
|
|
32
|
-
outcome: {
|
|
33
|
-
kind: ActivityOutcomeKind.Done,
|
|
34
|
-
data: { deliveryEventId: delivery.eventId },
|
|
35
|
-
},
|
|
36
|
-
}, {
|
|
37
|
-
commandId: delivery.eventId,
|
|
38
|
-
correlationId: event.correlationId,
|
|
39
|
-
actor: { kind: EventActorKind.System, id: 'delivery-outcome-reactor' },
|
|
40
|
-
occurredAt: event.recordedAt,
|
|
41
|
-
});
|
|
42
|
-
if (delivery.eventType === DeliveryEventType.Failed)
|
|
43
|
-
await this.orchestration.acceptOutcome({
|
|
44
|
-
...command,
|
|
45
|
-
outcome: {
|
|
46
|
-
kind: ActivityOutcomeKind.Failed,
|
|
47
|
-
data: { reason: delivery.payload.code },
|
|
48
|
-
},
|
|
49
|
-
}, {
|
|
50
|
-
commandId: delivery.eventId,
|
|
51
|
-
correlationId: event.correlationId,
|
|
52
|
-
actor: { kind: EventActorKind.System, id: 'delivery-outcome-reactor' },
|
|
53
|
-
occurredAt: event.recordedAt,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
21
|
+
await this.reconcile(event, resolvedDeliveryEventIds);
|
|
57
22
|
await this.checkpoints.save(consumer, event.globalPosition);
|
|
58
23
|
}
|
|
24
|
+
for (const event of await this.journal.readAll(0))
|
|
25
|
+
await this.reconcile(event, resolvedDeliveryEventIds);
|
|
59
26
|
return events.length;
|
|
60
27
|
}
|
|
28
|
+
async reconcile(event, seen) {
|
|
29
|
+
const delivery = selectDeliveryEvent(event);
|
|
30
|
+
if (delivery === null || seen.has(delivery.eventId))
|
|
31
|
+
return;
|
|
32
|
+
const outcome = delivery.eventType === DeliveryEventType.Confirmed ||
|
|
33
|
+
(delivery.eventType === DeliveryEventType.Reconciled &&
|
|
34
|
+
delivery.payload.result === DeliveryResultKind.Confirmed)
|
|
35
|
+
? { kind: ActivityOutcomeKind.Done, data: { deliveryEventId: delivery.eventId } }
|
|
36
|
+
: delivery.eventType === DeliveryEventType.Failed
|
|
37
|
+
? { kind: ActivityOutcomeKind.Failed, data: { reason: delivery.payload.code } }
|
|
38
|
+
: null;
|
|
39
|
+
if (outcome === null)
|
|
40
|
+
return;
|
|
41
|
+
const command = {
|
|
42
|
+
workflowInstanceId: workflowInstanceId(delivery.payload.workflowInstanceId),
|
|
43
|
+
activationId: activationId(delivery.payload.activationId),
|
|
44
|
+
};
|
|
45
|
+
if (!(await this.isAwaitingThisDelivery(command, delivery.payload.intentEventId)))
|
|
46
|
+
return;
|
|
47
|
+
seen.add(delivery.eventId);
|
|
48
|
+
await this.orchestration.acceptOutcome({ ...command, outcome }, {
|
|
49
|
+
commandId: delivery.eventId,
|
|
50
|
+
correlationId: event.correlationId,
|
|
51
|
+
actor: { kind: EventActorKind.System, id: 'delivery-outcome-reactor' },
|
|
52
|
+
occurredAt: event.recordedAt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
61
55
|
/**
|
|
62
56
|
* A delivery's own completion may only resolve the activation that
|
|
63
57
|
* actually asked to wait on it (an activity outcome of `waiting` with
|
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
import { dirname, normalize } from 'node:path/posix';
|
|
2
|
+
import { DockerProcessError, } from './docker-invocation.js';
|
|
2
3
|
import { scrubProcessLog } from './process-log.js';
|
|
4
|
+
export { DockerProcessError, } from './docker-invocation.js';
|
|
5
|
+
export { verifyResidentStart } from './resident-start.js';
|
|
6
|
+
const emptyDockerInvocationResult = { stdout: '', stderr: '' };
|
|
3
7
|
export function createDockerCli(invoke) {
|
|
4
|
-
return {
|
|
8
|
+
return {
|
|
9
|
+
async invoke(arguments_, options) {
|
|
10
|
+
return (await invoke(arguments_, options)) ?? emptyDockerInvocationResult;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
5
13
|
}
|
|
6
14
|
/** Streams Docker output through the target-owned scrubbed log boundary as it arrives. */
|
|
7
15
|
export function createLoggedDockerCli(process, log) {
|
|
8
16
|
return {
|
|
9
17
|
async invoke(arguments_, options) {
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
let stdout = '';
|
|
19
|
+
let stderr = '';
|
|
20
|
+
const onChunk = (chunk) => {
|
|
21
|
+
if (chunk.stream === 'stdout')
|
|
22
|
+
stdout += chunk.text;
|
|
23
|
+
else
|
|
24
|
+
stderr += chunk.text;
|
|
25
|
+
if (options?.suppressOutput || chunk.text.length === 0)
|
|
12
26
|
return;
|
|
13
27
|
return log.write(scrubProcessLog(chunk.text));
|
|
14
|
-
}
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const result = await process.execute(arguments_, onChunk, options);
|
|
31
|
+
return mergeDockerOutput(result, { stdout, stderr });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const result = mergeDockerOutput(error instanceof DockerProcessError ? error.result : undefined, { stdout, stderr });
|
|
35
|
+
throw new DockerProcessError(error instanceof Error ? error.message : String(error), result, error);
|
|
36
|
+
}
|
|
15
37
|
},
|
|
16
38
|
};
|
|
17
39
|
}
|
|
40
|
+
function mergeDockerOutput(result, captured) {
|
|
41
|
+
if (result === undefined)
|
|
42
|
+
return captured;
|
|
43
|
+
return {
|
|
44
|
+
stdout: result.stdout.length === 0 ? captured.stdout : result.stdout,
|
|
45
|
+
stderr: result.stderr.length === 0 ? captured.stderr : result.stderr,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
18
48
|
/** Bounded target sandbox lifecycle; domain modules never name Docker. */
|
|
19
49
|
const dockerRunCommand = String.fromCharCode(114, 117, 110);
|
|
20
50
|
// The bounded lifecycle is intentionally co-located for command parity.
|
|
@@ -108,34 +138,6 @@ export function createSandboxDockerPort(docker, options) {
|
|
|
108
138
|
},
|
|
109
139
|
};
|
|
110
140
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Polls until the resident `wake start` process is alive inside the
|
|
113
|
-
* container (pid file + live pid + expected cmdline), retrying on any
|
|
114
|
-
* failure — `docker.invoke` already throws on a non-zero exit, so each
|
|
115
|
-
* attempt is just resolve-or-throw. Used after a self-update container swap
|
|
116
|
-
* to confirm the new image actually came up before trusting it.
|
|
117
|
-
*/
|
|
118
|
-
export async function verifyResidentStart(docker, containerName, expectedCmdlineFragment, options) {
|
|
119
|
-
const attempts = options?.attempts ?? 15;
|
|
120
|
-
const intervalMs = options?.intervalMs ?? 1000;
|
|
121
|
-
const check = [
|
|
122
|
-
'pid="$(cat /wake/.wake/logs/start.pid)"',
|
|
123
|
-
'test -n "$pid"',
|
|
124
|
-
'kill -0 "$pid"',
|
|
125
|
-
`tr '\\0' ' ' < "/proc/$pid/cmdline" | grep -F ${shellQuote(expectedCmdlineFragment)} >/dev/null`,
|
|
126
|
-
].join(' && ');
|
|
127
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
128
|
-
try {
|
|
129
|
-
await docker.invoke(['exec', '-i', containerName, 'sh', '-lc', check]);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
if (attempt === attempts)
|
|
134
|
-
throw error;
|
|
135
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
141
|
function shellQuote(value) {
|
|
140
142
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
141
143
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A failed Docker invocation whose streamed output remains available for a concise caller diagnostic. */
|
|
2
|
+
export class DockerProcessError extends Error {
|
|
3
|
+
result;
|
|
4
|
+
constructor(message, result, cause) {
|
|
5
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
6
|
+
this.result = result;
|
|
7
|
+
this.name = 'DockerProcessError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DockerProcessError, } from './docker-invocation.js';
|
|
2
|
+
/** Verifies the replacement sandbox has started its resident Wake process. */
|
|
3
|
+
export async function verifyResidentStart(docker, containerName, expectedCmdlineFragment, options) {
|
|
4
|
+
const attempts = options?.attempts ?? 15;
|
|
5
|
+
const intervalMs = options?.intervalMs ?? 1000;
|
|
6
|
+
const check = [
|
|
7
|
+
'pid="$(cat /wake/.wake/logs/start.pid)"',
|
|
8
|
+
'test -n "$pid"',
|
|
9
|
+
'kill -0 "$pid"',
|
|
10
|
+
`tr '\\0' ' ' < "/proc/$pid/cmdline" | grep -F ${shellQuote(expectedCmdlineFragment)} >/dev/null`,
|
|
11
|
+
].join(' && ');
|
|
12
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
13
|
+
try {
|
|
14
|
+
await docker.invoke(['exec', '-i', containerName, 'sh', '-lc', check], {
|
|
15
|
+
suppressOutput: attempt < attempts,
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (attempt === attempts)
|
|
21
|
+
throw residentStartFailure(attempts, error);
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function residentStartFailure(attempts, error) {
|
|
27
|
+
const detail = error instanceof DockerProcessError
|
|
28
|
+
? [
|
|
29
|
+
error.message,
|
|
30
|
+
error.result.stdout.length === 0 ? '' : `stdout: ${error.result.stdout.trim()}`,
|
|
31
|
+
error.result.stderr.length === 0 ? '' : `stderr: ${error.result.stderr.trim()}`,
|
|
32
|
+
]
|
|
33
|
+
.filter((value) => value.length > 0)
|
|
34
|
+
.join('; ')
|
|
35
|
+
: error instanceof Error
|
|
36
|
+
? error.message
|
|
37
|
+
: String(error);
|
|
38
|
+
return new Error(`Wake resident start did not become healthy after ${String(attempts)} attempts: ${detail}`);
|
|
39
|
+
}
|
|
40
|
+
function shellQuote(value) {
|
|
41
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
42
|
+
}
|