@atolis-hq/wake 0.2.28 → 0.2.29
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.
|
@@ -8,22 +8,111 @@ import { createEventEnvelope } from '../lib/event-log.js';
|
|
|
8
8
|
// `deliverOutboundEvent` is injected rather than imported so this module does
|
|
9
9
|
// not depend on the outbox module directly.
|
|
10
10
|
export function createStaleRunReconciler(deps) {
|
|
11
|
+
function isTerminalLifecycle(lifecycle) {
|
|
12
|
+
return lifecycle === 'TERMINAL';
|
|
13
|
+
}
|
|
14
|
+
function recoveryOutcomeForLifecycle(lifecycle) {
|
|
15
|
+
switch (lifecycle) {
|
|
16
|
+
case 'CREATED':
|
|
17
|
+
case 'CLAIMED':
|
|
18
|
+
case 'PREPARING':
|
|
19
|
+
return 'CANCELED_BY_RECONCILIATION';
|
|
20
|
+
case 'PROCESS_STARTING':
|
|
21
|
+
case 'RUNNING':
|
|
22
|
+
case 'FINALISING':
|
|
23
|
+
return 'STALLED';
|
|
24
|
+
case 'TERMINAL':
|
|
25
|
+
return 'SUPERSEDED';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
11
28
|
async function staleReason(record, now) {
|
|
12
29
|
if (record.status !== 'running') {
|
|
13
30
|
return null;
|
|
14
31
|
}
|
|
15
|
-
if (
|
|
32
|
+
if (isTerminalLifecycle(record.lifecycle)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (deps.isRunningRecordActive === undefined) {
|
|
36
|
+
if (record.lifecycle !== 'CREATED' &&
|
|
37
|
+
record.lifecycle !== 'CLAIMED' &&
|
|
38
|
+
record.lifecycle !== 'PREPARING') {
|
|
39
|
+
const startedAtMs = Date.parse(record.startedAt);
|
|
40
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
41
|
+
return 'timeout';
|
|
42
|
+
}
|
|
43
|
+
return now.getTime() - startedAtMs >= deps.runnerTimeoutMs() ? 'timeout' : null;
|
|
44
|
+
}
|
|
45
|
+
return 'startup-recovery';
|
|
46
|
+
}
|
|
47
|
+
const active = await deps.isRunningRecordActive(record);
|
|
48
|
+
if (active) {
|
|
49
|
+
const startedAtMs = Date.parse(record.startedAt);
|
|
50
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
51
|
+
return 'timeout';
|
|
52
|
+
}
|
|
53
|
+
return now.getTime() - startedAtMs >= deps.runnerTimeoutMs() ? 'timeout' : null;
|
|
54
|
+
}
|
|
55
|
+
if (record.lifecycle === 'RUNNING' || record.lifecycle === 'PROCESS_STARTING') {
|
|
16
56
|
return 'runner-lock-not-active';
|
|
17
57
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
58
|
+
return 'startup-recovery';
|
|
59
|
+
}
|
|
60
|
+
async function deliverRecoveredLabels(projection, runId, finishedAt) {
|
|
61
|
+
await deps.deliverOutboundEvent(createLabelsEvent({
|
|
62
|
+
projection,
|
|
63
|
+
runId,
|
|
64
|
+
statusLabel: 'wake:status.failed',
|
|
65
|
+
stageLabel: stageLabelForStage(projection.wake.stage),
|
|
66
|
+
workflowLabel: workflowLabelForWorkflowName(workflowNameForProjection(projection, deps.config)),
|
|
67
|
+
occurredAt: finishedAt,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
async function recoverMissingRunRecordClaims(runRecords, finishedAt) {
|
|
71
|
+
const knownRunIds = new Set(runRecords.map((record) => record.runId));
|
|
72
|
+
const projections = await deps.stateStore.listIssueStates();
|
|
73
|
+
for (const projection of projections) {
|
|
74
|
+
const runId = projection.wake.lastRunId;
|
|
75
|
+
if (runId === undefined ||
|
|
76
|
+
knownRunIds.has(runId) ||
|
|
77
|
+
runRecords.some((record) => record.workItemKey === projection.workItemKey) ||
|
|
78
|
+
projection.context.lastRunSentinel !== undefined) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const event = createEventEnvelope({
|
|
82
|
+
eventId: `${runId}-missing-run-record-recovered`,
|
|
83
|
+
workItemKey: projection.workItemKey,
|
|
84
|
+
streamScope: 'work-item',
|
|
85
|
+
direction: 'internal',
|
|
86
|
+
sourceSystem: 'wake',
|
|
87
|
+
sourceEventType: 'wake.run.completed',
|
|
88
|
+
sourceRefs: {
|
|
89
|
+
repo: projection.issue.repo,
|
|
90
|
+
issueNumber: projection.issue.number,
|
|
91
|
+
runId,
|
|
92
|
+
},
|
|
93
|
+
occurredAt: finishedAt,
|
|
94
|
+
ingestedAt: finishedAt,
|
|
95
|
+
trigger: 'immediate',
|
|
96
|
+
payload: {
|
|
97
|
+
action: projection.context.lastRunAction,
|
|
98
|
+
sentinel: 'FAILED',
|
|
99
|
+
executionOutcome: 'CANCELED_BY_RECONCILIATION',
|
|
100
|
+
runId,
|
|
101
|
+
reason: 'runner:missing-run-record',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
await deps.stateStore.appendEventEnvelope(event);
|
|
105
|
+
await deps.projectionUpdater.rebuildFromEvents([event]);
|
|
106
|
+
const updatedProjection = await deps.stateStore.readIssueState(projection.workItemKey);
|
|
107
|
+
if (updatedProjection !== null) {
|
|
108
|
+
await deliverRecoveredLabels(updatedProjection, runId, finishedAt);
|
|
109
|
+
}
|
|
21
110
|
}
|
|
22
|
-
return now.getTime() - startedAtMs >= deps.runnerTimeoutMs() ? 'timeout' : null;
|
|
23
111
|
}
|
|
24
112
|
async function reconcileStaleRunningRecords(now) {
|
|
25
113
|
const finishedAt = now.toISOString();
|
|
26
114
|
const runRecords = await deps.stateStore.listRunRecords();
|
|
115
|
+
await recoverMissingRunRecordClaims(runRecords, finishedAt);
|
|
27
116
|
const staleRecords = [];
|
|
28
117
|
for (const record of runRecords) {
|
|
29
118
|
const reason = await staleReason(record, now);
|
|
@@ -46,6 +135,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
46
135
|
if (projection === null || projection.wake.lastRunId !== record.runId || newerCompletedRun) {
|
|
47
136
|
await deps.stateStore.writeRunRecord({
|
|
48
137
|
...record,
|
|
138
|
+
lifecycle: 'TERMINAL',
|
|
49
139
|
status: 'superseded',
|
|
50
140
|
finishedAt,
|
|
51
141
|
executionOutcome: 'SUPERSEDED',
|
|
@@ -58,9 +148,10 @@ export function createStaleRunReconciler(deps) {
|
|
|
58
148
|
});
|
|
59
149
|
continue;
|
|
60
150
|
}
|
|
61
|
-
const staleExecutionOutcome = reason === 'timeout' ? 'TIMED_OUT' :
|
|
151
|
+
const staleExecutionOutcome = reason === 'timeout' ? 'TIMED_OUT' : recoveryOutcomeForLifecycle(record.lifecycle);
|
|
62
152
|
await deps.stateStore.writeRunRecord({
|
|
63
153
|
...record,
|
|
154
|
+
lifecycle: 'TERMINAL',
|
|
64
155
|
status: 'failed',
|
|
65
156
|
finishedAt,
|
|
66
157
|
sentinel: 'FAILED',
|
|
@@ -69,6 +160,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
69
160
|
metadata: {
|
|
70
161
|
...record.metadata,
|
|
71
162
|
reconciledBy: 'stale-running-record',
|
|
163
|
+
recoveryLifecycle: record.lifecycle,
|
|
72
164
|
staleReason: reason,
|
|
73
165
|
timeoutMs: deps.runnerTimeoutMs(),
|
|
74
166
|
},
|
|
@@ -93,7 +185,11 @@ export function createStaleRunReconciler(deps) {
|
|
|
93
185
|
sentinel: 'FAILED',
|
|
94
186
|
executionOutcome: staleExecutionOutcome,
|
|
95
187
|
runId: record.runId,
|
|
96
|
-
reason: reason === 'timeout'
|
|
188
|
+
reason: reason === 'timeout'
|
|
189
|
+
? 'runner:stale-timeout'
|
|
190
|
+
: reason === 'startup-recovery'
|
|
191
|
+
? `runner:recover-${record.lifecycle.toLowerCase()}`
|
|
192
|
+
: 'runner:orphaned-process',
|
|
97
193
|
...(record.routing === undefined ? {} : { routing: record.routing }),
|
|
98
194
|
},
|
|
99
195
|
});
|
|
@@ -101,14 +197,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
101
197
|
await deps.projectionUpdater.rebuildFromEvents([runCompletedEvent]);
|
|
102
198
|
const updatedProjection = await deps.stateStore.readIssueState(projection.workItemKey);
|
|
103
199
|
if (updatedProjection !== null) {
|
|
104
|
-
await
|
|
105
|
-
projection: updatedProjection,
|
|
106
|
-
runId: record.runId,
|
|
107
|
-
statusLabel: 'wake:status.failed',
|
|
108
|
-
stageLabel: stageLabelForStage(updatedProjection.wake.stage),
|
|
109
|
-
workflowLabel: workflowLabelForWorkflowName(workflowNameForProjection(updatedProjection, deps.config)),
|
|
110
|
-
occurredAt: finishedAt,
|
|
111
|
-
}));
|
|
200
|
+
await deliverRecoveredLabels(updatedProjection, record.runId, finishedAt);
|
|
112
201
|
}
|
|
113
202
|
}
|
|
114
203
|
}
|
|
@@ -446,10 +446,18 @@ export function createTickRunner(deps) {
|
|
|
446
446
|
repo: candidate.issue.repo,
|
|
447
447
|
issueNumber: candidate.issue.number,
|
|
448
448
|
action,
|
|
449
|
+
lifecycle: 'CREATED',
|
|
449
450
|
status: 'running',
|
|
450
451
|
startedAt: nowIso,
|
|
452
|
+
routing,
|
|
451
453
|
};
|
|
452
454
|
await deps.stateStore.writeRunRecord(runningRecord);
|
|
455
|
+
async function transitionRunLifecycle(lifecycle) {
|
|
456
|
+
await deps.stateStore.writeRunRecord({
|
|
457
|
+
...(await deps.stateStore.readRunRecord(runId)),
|
|
458
|
+
lifecycle,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
453
461
|
const claimedAt = eventStampNow();
|
|
454
462
|
const claimedEvent = createEventEnvelope({
|
|
455
463
|
eventId: `${runId}-claimed`,
|
|
@@ -473,6 +481,7 @@ export function createTickRunner(deps) {
|
|
|
473
481
|
},
|
|
474
482
|
});
|
|
475
483
|
await deps.stateStore.appendEventEnvelope(claimedEvent);
|
|
484
|
+
await transitionRunLifecycle('CLAIMED');
|
|
476
485
|
await projectionUpdater.rebuildFromEvents([claimedEvent]);
|
|
477
486
|
await deliverOutboundEvent(createLabelsEvent({
|
|
478
487
|
projection: candidate,
|
|
@@ -483,6 +492,7 @@ export function createTickRunner(deps) {
|
|
|
483
492
|
occurredAt: eventStampNow(),
|
|
484
493
|
}));
|
|
485
494
|
try {
|
|
495
|
+
await transitionRunLifecycle('PREPARING');
|
|
486
496
|
const prepareResult = workspaceMode === 'branch'
|
|
487
497
|
? await deps.workspaceManager.prepareWorkspace({
|
|
488
498
|
workId: candidate.workItemKey,
|
|
@@ -499,7 +509,18 @@ export function createTickRunner(deps) {
|
|
|
499
509
|
const upstreamChanges = 'upstreamChanges' in prepareResult && typeof prepareResult.upstreamChanges === 'string'
|
|
500
510
|
? prepareResult.upstreamChanges
|
|
501
511
|
: undefined;
|
|
512
|
+
const preparedRecord = (await deps.stateStore.readRunRecord(runId));
|
|
513
|
+
await deps.stateStore.writeRunRecord({
|
|
514
|
+
...preparedRecord,
|
|
515
|
+
lifecycle: 'PROCESS_STARTING',
|
|
516
|
+
metadata: {
|
|
517
|
+
...preparedRecord.metadata,
|
|
518
|
+
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
519
|
+
workspaceMode,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
502
522
|
const recentEvents = await deps.stateStore.listEventEnvelopesForWorkItem(candidate.workItemKey, 6);
|
|
523
|
+
await transitionRunLifecycle('RUNNING');
|
|
503
524
|
const runnerResult = await deps.runner.run({
|
|
504
525
|
action,
|
|
505
526
|
projection: candidate,
|
|
@@ -583,8 +604,11 @@ export function createTickRunner(deps) {
|
|
|
583
604
|
: sentinel === 'AWAITING_APPROVAL'
|
|
584
605
|
? 'AWAITING_APPROVAL'
|
|
585
606
|
: undefined;
|
|
607
|
+
await transitionRunLifecycle('FINALISING');
|
|
608
|
+
const finalisingRecord = (await deps.stateStore.readRunRecord(runId));
|
|
586
609
|
await deps.stateStore.writeRunRecord({
|
|
587
|
-
...
|
|
610
|
+
...finalisingRecord,
|
|
611
|
+
lifecycle: 'TERMINAL',
|
|
588
612
|
status: sentinel === 'DONE'
|
|
589
613
|
? 'completed'
|
|
590
614
|
: sentinel === 'BLOCKED'
|
|
@@ -600,7 +624,10 @@ export function createTickRunner(deps) {
|
|
|
600
624
|
summary: parsedRunnerResult.body,
|
|
601
625
|
...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
|
|
602
626
|
...(runnerResult.tokenUsage === undefined ? {} : { tokenUsage: runnerResult.tokenUsage }),
|
|
603
|
-
metadata:
|
|
627
|
+
metadata: {
|
|
628
|
+
...finalisingRecord.metadata,
|
|
629
|
+
...resultMetadata,
|
|
630
|
+
},
|
|
604
631
|
});
|
|
605
632
|
const runCompletedEvent = createEventEnvelope({
|
|
606
633
|
eventId: `${runId}-completed`,
|
|
@@ -684,8 +711,10 @@ export function createTickRunner(deps) {
|
|
|
684
711
|
catch (err) {
|
|
685
712
|
const finishedAt = deps.clock.now().toISOString();
|
|
686
713
|
const sentinel = 'FAILED';
|
|
714
|
+
const failedRecord = (await deps.stateStore.readRunRecord(runId)) ?? runningRecord;
|
|
687
715
|
await deps.stateStore.writeRunRecord({
|
|
688
|
-
...
|
|
716
|
+
...failedRecord,
|
|
717
|
+
lifecycle: 'TERMINAL',
|
|
689
718
|
status: 'failed',
|
|
690
719
|
finishedAt,
|
|
691
720
|
sentinel,
|
|
@@ -34,8 +34,18 @@ export const workflowOutcomeValues = [
|
|
|
34
34
|
'AWAITING_INPUT',
|
|
35
35
|
'CHANGES_REQUESTED',
|
|
36
36
|
];
|
|
37
|
+
export const executionAttemptLifecycleValues = [
|
|
38
|
+
'CREATED',
|
|
39
|
+
'CLAIMED',
|
|
40
|
+
'PREPARING',
|
|
41
|
+
'PROCESS_STARTING',
|
|
42
|
+
'RUNNING',
|
|
43
|
+
'FINALISING',
|
|
44
|
+
'TERMINAL',
|
|
45
|
+
];
|
|
37
46
|
export const executionOutcomeSchema = z.enum(executionOutcomeValues);
|
|
38
47
|
export const workflowOutcomeSchema = z.enum(workflowOutcomeValues);
|
|
48
|
+
export const executionAttemptLifecycleSchema = z.enum(executionAttemptLifecycleValues);
|
|
39
49
|
export const defaultAgentIdentity = 'Wake';
|
|
40
50
|
export const defaultSmokePrompt = `This is ${defaultAgentIdentity}, reply with "hi ${defaultAgentIdentity} only"`;
|
|
41
51
|
const modelOverridesSchema = z
|
|
@@ -291,7 +301,20 @@ const runTokenUsageSchema = z.object({
|
|
|
291
301
|
costUsd: z.number().nonnegative().optional(),
|
|
292
302
|
turns: z.number().nonnegative().optional(),
|
|
293
303
|
});
|
|
294
|
-
|
|
304
|
+
function legacyRunLifecycle(input) {
|
|
305
|
+
if (input.lifecycle !== undefined) {
|
|
306
|
+
return input;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
...input,
|
|
310
|
+
lifecycle: input.status === 'running' ? 'RUNNING' : 'TERMINAL',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export const runRecordSchema = z.preprocess((input) => {
|
|
314
|
+
return input !== null && typeof input === 'object'
|
|
315
|
+
? legacyRunLifecycle(input)
|
|
316
|
+
: input;
|
|
317
|
+
}, z.object({
|
|
295
318
|
schemaVersion: z.literal(1),
|
|
296
319
|
runId: z.string(),
|
|
297
320
|
// The work item this run belongs to. Required: run records are Wake-owned
|
|
@@ -306,7 +329,15 @@ export const runRecordSchema = z.object({
|
|
|
306
329
|
repo: z.string(),
|
|
307
330
|
issueNumber: z.number().int().positive(),
|
|
308
331
|
action: identifierSchema,
|
|
309
|
-
|
|
332
|
+
lifecycle: executionAttemptLifecycleSchema,
|
|
333
|
+
status: z.enum([
|
|
334
|
+
'running',
|
|
335
|
+
'completed',
|
|
336
|
+
'awaiting-approval',
|
|
337
|
+
'blocked',
|
|
338
|
+
'failed',
|
|
339
|
+
'superseded',
|
|
340
|
+
]),
|
|
310
341
|
startedAt: isoTimestampSchema,
|
|
311
342
|
finishedAt: isoTimestampSchema.optional(),
|
|
312
343
|
sessionId: z.string().optional(),
|
|
@@ -317,7 +348,7 @@ export const runRecordSchema = z.object({
|
|
|
317
348
|
routing: runnerRoutingSchema.optional(),
|
|
318
349
|
tokenUsage: runTokenUsageSchema.optional(),
|
|
319
350
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
320
|
-
});
|
|
351
|
+
}));
|
|
321
352
|
const runnerHealthEntrySchema = z.object({
|
|
322
353
|
pausedUntil: isoTimestampSchema.optional(),
|
|
323
354
|
// 'reported' = CLI told us the real reset time; 'estimated' = exponential
|
package/dist/src/version.js
CHANGED