@atolis-hq/wake 0.2.51 → 0.2.52
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.
|
@@ -110,7 +110,7 @@ export function createPolicyEngine() {
|
|
|
110
110
|
const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
|
|
111
111
|
const lastCompletedAction = typeof context.lastCompletedAction === 'string' ? context.lastCompletedAction : undefined;
|
|
112
112
|
const lastRunSentinel = typeof context.lastRunSentinel === 'string' ? context.lastRunSentinel : undefined;
|
|
113
|
-
const
|
|
113
|
+
const lastRetrySafety = typeof context.lastRetrySafety === 'string' ? context.lastRetrySafety : undefined;
|
|
114
114
|
if (issue.wake.lastRunId === undefined) {
|
|
115
115
|
return true;
|
|
116
116
|
}
|
|
@@ -122,15 +122,15 @@ export function createPolicyEngine() {
|
|
|
122
122
|
if (isAwaitingApproval(issue)) {
|
|
123
123
|
return false;
|
|
124
124
|
}
|
|
125
|
-
if (lastRunSentinel === failedRunnerSentinel
|
|
125
|
+
if (lastRunSentinel === failedRunnerSentinel) {
|
|
126
|
+
if (lastRetrySafety === 'SAFE_TO_RETRY' || lastRetrySafety === 'SAFE_TO_RESUME') {
|
|
127
|
+
return belowFailureRetryLimit(issue, config);
|
|
128
|
+
}
|
|
126
129
|
return false;
|
|
127
130
|
}
|
|
128
131
|
if (lastRunSentinel === 'BLOCKED') {
|
|
129
132
|
return false;
|
|
130
133
|
}
|
|
131
|
-
if (lastFailureClass === 'quota') {
|
|
132
|
-
return belowFailureRetryLimit(issue, config);
|
|
133
|
-
}
|
|
134
134
|
const workflowAction = chooseWorkflowAction(issue, workflow);
|
|
135
135
|
return workflowAction !== null && lastCompletedAction !== workflowAction.action;
|
|
136
136
|
},
|
|
@@ -253,6 +253,17 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
253
253
|
...(payload.workflowOutcome !== undefined
|
|
254
254
|
? { lastWorkflowOutcome: payload.workflowOutcome }
|
|
255
255
|
: {}),
|
|
256
|
+
...(payload.failurePhase !== undefined ? { lastFailurePhase: payload.failurePhase } : {}),
|
|
257
|
+
...(payload.processStarted !== undefined
|
|
258
|
+
? { lastProcessStarted: payload.processStarted }
|
|
259
|
+
: {}),
|
|
260
|
+
...(payload.workspaceChanged !== undefined
|
|
261
|
+
? { lastWorkspaceChanged: payload.workspaceChanged }
|
|
262
|
+
: {}),
|
|
263
|
+
...(payload.externalSideEffects !== undefined
|
|
264
|
+
? { lastExternalSideEffects: payload.externalSideEffects }
|
|
265
|
+
: {}),
|
|
266
|
+
...(payload.retrySafety !== undefined ? { lastRetrySafety: payload.retrySafety } : {}),
|
|
256
267
|
};
|
|
257
268
|
if (payload.sentinel === 'BLOCKED' || payload.sentinel === 'FAILED') {
|
|
258
269
|
nextContext.blockedFromStage = current.wake.stage;
|
|
@@ -26,6 +26,37 @@ export function createStaleRunReconciler(deps) {
|
|
|
26
26
|
return 'SUPERSEDED';
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
function failurePhaseForLifecycle(lifecycle) {
|
|
30
|
+
switch (lifecycle) {
|
|
31
|
+
case 'CREATED':
|
|
32
|
+
case 'CLAIMED':
|
|
33
|
+
case 'PREPARING':
|
|
34
|
+
return 'workspace-prep';
|
|
35
|
+
case 'PROCESS_STARTING':
|
|
36
|
+
return 'process-starting';
|
|
37
|
+
case 'RUNNING':
|
|
38
|
+
case 'FINALISING':
|
|
39
|
+
return 'running';
|
|
40
|
+
case 'TERMINAL':
|
|
41
|
+
return 'unknown';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function classifyReconciledFailure(record) {
|
|
45
|
+
const processStarted = record.agentPid !== undefined ||
|
|
46
|
+
record.agentProcessStartedAt !== undefined ||
|
|
47
|
+
record.lifecycle === 'RUNNING' ||
|
|
48
|
+
record.lifecycle === 'FINALISING';
|
|
49
|
+
const metadata = record.metadata;
|
|
50
|
+
const workspaceChanged = typeof metadata?.workspacePath === 'string';
|
|
51
|
+
const retrySafety = processStarted || workspaceChanged ? 'REQUIRES_RECONCILIATION' : 'SAFE_TO_RETRY';
|
|
52
|
+
return {
|
|
53
|
+
failurePhase: failurePhaseForLifecycle(record.lifecycle),
|
|
54
|
+
processStarted,
|
|
55
|
+
workspaceChanged,
|
|
56
|
+
externalSideEffects: processStarted ? 'unknown' : 'none',
|
|
57
|
+
retrySafety,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
29
60
|
async function staleReason(record, now) {
|
|
30
61
|
if (record.status !== 'running') {
|
|
31
62
|
return null;
|
|
@@ -94,6 +125,11 @@ export function createStaleRunReconciler(deps) {
|
|
|
94
125
|
action: projection.context.lastRunAction,
|
|
95
126
|
sentinel: 'FAILED',
|
|
96
127
|
executionOutcome: 'CANCELED_BY_RECONCILIATION',
|
|
128
|
+
failurePhase: 'unknown',
|
|
129
|
+
processStarted: false,
|
|
130
|
+
workspaceChanged: false,
|
|
131
|
+
externalSideEffects: 'unknown',
|
|
132
|
+
retrySafety: 'MANUAL_REVIEW_REQUIRED',
|
|
97
133
|
runId,
|
|
98
134
|
reason: 'runner:missing-run-record',
|
|
99
135
|
},
|
|
@@ -146,6 +182,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
146
182
|
continue;
|
|
147
183
|
}
|
|
148
184
|
const staleExecutionOutcome = reason === 'timeout' ? 'TIMED_OUT' : recoveryOutcomeForLifecycle(record.lifecycle);
|
|
185
|
+
const failureContext = classifyReconciledFailure(record);
|
|
149
186
|
await deps.stateStore.writeRunRecord({
|
|
150
187
|
...record,
|
|
151
188
|
lifecycle: 'TERMINAL',
|
|
@@ -153,6 +190,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
153
190
|
finishedAt,
|
|
154
191
|
sentinel: 'FAILED',
|
|
155
192
|
executionOutcome: staleExecutionOutcome,
|
|
193
|
+
...failureContext,
|
|
156
194
|
summary: `Run exceeded timeout while marked running and was reconciled by a later tick.`,
|
|
157
195
|
metadata: {
|
|
158
196
|
...record.metadata,
|
|
@@ -160,6 +198,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
160
198
|
recoveryLifecycle: record.lifecycle,
|
|
161
199
|
staleReason: reason,
|
|
162
200
|
timeoutMs: deps.runnerTimeoutMs(),
|
|
201
|
+
...failureContext,
|
|
163
202
|
},
|
|
164
203
|
});
|
|
165
204
|
const runCompletedEvent = createEventEnvelope({
|
|
@@ -181,6 +220,7 @@ export function createStaleRunReconciler(deps) {
|
|
|
181
220
|
action: record.action,
|
|
182
221
|
sentinel: 'FAILED',
|
|
183
222
|
executionOutcome: staleExecutionOutcome,
|
|
223
|
+
...failureContext,
|
|
184
224
|
runId: record.runId,
|
|
185
225
|
reason: reason === 'timeout'
|
|
186
226
|
? 'runner:stale-timeout'
|
|
@@ -52,6 +52,54 @@ function shouldPublishRunResult(input) {
|
|
|
52
52
|
}
|
|
53
53
|
return input.failureClass !== input.previousFailureClass;
|
|
54
54
|
}
|
|
55
|
+
function hasConfirmedExternalSideEffect(projection) {
|
|
56
|
+
return projection.correlatedResources.some((resource) => /^[a-z0-9-]+:pr:/.test(resource.resourceUri));
|
|
57
|
+
}
|
|
58
|
+
function failurePhaseForRecord(record) {
|
|
59
|
+
if (record.agentPid !== undefined || record.agentProcessStartedAt !== undefined) {
|
|
60
|
+
return 'running';
|
|
61
|
+
}
|
|
62
|
+
if (record.lifecycle === 'PROCESS_STARTING' || record.lifecycle === 'RUNNING') {
|
|
63
|
+
return 'process-starting';
|
|
64
|
+
}
|
|
65
|
+
if (record.lifecycle === 'PREPARING') {
|
|
66
|
+
return 'workspace-prep';
|
|
67
|
+
}
|
|
68
|
+
return 'unknown';
|
|
69
|
+
}
|
|
70
|
+
function classifyFailedRun(input) {
|
|
71
|
+
const processStarted = input.record.agentPid !== undefined || input.record.agentProcessStartedAt !== undefined;
|
|
72
|
+
const workspaceChanged = input.workspacePath !== undefined;
|
|
73
|
+
const externalSideEffects = hasConfirmedExternalSideEffect(input.projection)
|
|
74
|
+
? 'confirmed'
|
|
75
|
+
: processStarted
|
|
76
|
+
? 'unknown'
|
|
77
|
+
: 'none';
|
|
78
|
+
const failurePhase = input.failurePhase ??
|
|
79
|
+
(input.envelope === 'degraded' && input.sentinel === 'FAILED'
|
|
80
|
+
? 'result-parsing'
|
|
81
|
+
: failurePhaseForRecord(input.record));
|
|
82
|
+
let retrySafety;
|
|
83
|
+
if (input.failureClass === 'task') {
|
|
84
|
+
retrySafety = 'NOT_RETRYABLE';
|
|
85
|
+
}
|
|
86
|
+
else if (externalSideEffects === 'confirmed' || workspaceChanged) {
|
|
87
|
+
retrySafety = 'REQUIRES_RECONCILIATION';
|
|
88
|
+
}
|
|
89
|
+
else if (processStarted) {
|
|
90
|
+
retrySafety = 'SAFE_TO_RESUME';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
retrySafety = 'SAFE_TO_RETRY';
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
failurePhase,
|
|
97
|
+
processStarted,
|
|
98
|
+
workspaceChanged,
|
|
99
|
+
externalSideEffects,
|
|
100
|
+
retrySafety,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
55
103
|
export function createTickRunner(deps) {
|
|
56
104
|
const policy = createPolicyEngine();
|
|
57
105
|
const lifecycle = createLifecycleService();
|
|
@@ -1464,6 +1512,16 @@ export function createTickRunner(deps) {
|
|
|
1464
1512
|
: undefined;
|
|
1465
1513
|
await transitionRunLifecycle('FINALISING');
|
|
1466
1514
|
const finalisingRecord = (await deps.stateStore.readRunRecord(runId));
|
|
1515
|
+
const failureContext = sentinel === 'FAILED'
|
|
1516
|
+
? classifyFailedRun({
|
|
1517
|
+
projection: candidate,
|
|
1518
|
+
record: finalisingRecord,
|
|
1519
|
+
failureClass: runnerResult.failureClass ?? 'task',
|
|
1520
|
+
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
1521
|
+
envelope: parsedRunnerResult.envelope,
|
|
1522
|
+
sentinel,
|
|
1523
|
+
})
|
|
1524
|
+
: undefined;
|
|
1467
1525
|
await deps.stateStore.writeRunRecord({
|
|
1468
1526
|
...finalisingRecord,
|
|
1469
1527
|
lifecycle: 'TERMINAL',
|
|
@@ -1479,6 +1537,7 @@ export function createTickRunner(deps) {
|
|
|
1479
1537
|
sentinel,
|
|
1480
1538
|
executionOutcome,
|
|
1481
1539
|
...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
|
|
1540
|
+
...(failureContext === undefined ? {} : failureContext),
|
|
1482
1541
|
summary: parsedRunnerResult.body,
|
|
1483
1542
|
...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
|
|
1484
1543
|
...(runnerResult.tokenUsage === undefined ? {} : { tokenUsage: runnerResult.tokenUsage }),
|
|
@@ -1519,6 +1578,7 @@ export function createTickRunner(deps) {
|
|
|
1519
1578
|
...(runnerResult.failureClass === undefined
|
|
1520
1579
|
? {}
|
|
1521
1580
|
: { failureClass: runnerResult.failureClass }),
|
|
1581
|
+
...(failureContext === undefined ? {} : failureContext),
|
|
1522
1582
|
// Only mark the triggering comment handled when the run reached the
|
|
1523
1583
|
// agent and produced a real outcome. Quota/infra failures are transient
|
|
1524
1584
|
// blips, not an answer to the human's comment — leaving handledCommentId
|
|
@@ -1612,6 +1672,18 @@ export function createTickRunner(deps) {
|
|
|
1612
1672
|
const finishedAt = deps.clock.now().toISOString();
|
|
1613
1673
|
const sentinel = 'FAILED';
|
|
1614
1674
|
const failedRecord = (await deps.stateStore.readRunRecord(runId)) ?? runningRecord;
|
|
1675
|
+
const failedRecordMetadata = failedRecord.metadata;
|
|
1676
|
+
const failedRecordWorkspacePath = typeof failedRecordMetadata?.workspacePath === 'string'
|
|
1677
|
+
? failedRecordMetadata.workspacePath
|
|
1678
|
+
: undefined;
|
|
1679
|
+
const failureContext = classifyFailedRun({
|
|
1680
|
+
projection: candidate,
|
|
1681
|
+
record: failedRecord,
|
|
1682
|
+
failureClass: 'infra',
|
|
1683
|
+
...(failedRecordWorkspacePath === undefined
|
|
1684
|
+
? {}
|
|
1685
|
+
: { workspacePath: failedRecordWorkspacePath }),
|
|
1686
|
+
});
|
|
1615
1687
|
await deps.stateStore.writeRunRecord({
|
|
1616
1688
|
...failedRecord,
|
|
1617
1689
|
lifecycle: 'TERMINAL',
|
|
@@ -1619,10 +1691,12 @@ export function createTickRunner(deps) {
|
|
|
1619
1691
|
finishedAt,
|
|
1620
1692
|
sentinel,
|
|
1621
1693
|
executionOutcome: 'PROCESS_FAILED',
|
|
1694
|
+
...failureContext,
|
|
1622
1695
|
summary: err instanceof Error ? err.message : String(err),
|
|
1623
1696
|
metadata: {
|
|
1624
1697
|
...failedRecord.metadata,
|
|
1625
1698
|
failureClass: 'infra',
|
|
1699
|
+
...failureContext,
|
|
1626
1700
|
},
|
|
1627
1701
|
});
|
|
1628
1702
|
const runCompletedEvent = createEventEnvelope({
|
|
@@ -1646,6 +1720,7 @@ export function createTickRunner(deps) {
|
|
|
1646
1720
|
runId,
|
|
1647
1721
|
reason: 'runner:infrastructure-error',
|
|
1648
1722
|
failureClass: 'infra',
|
|
1723
|
+
...failureContext,
|
|
1649
1724
|
executionOutcome: 'PROCESS_FAILED',
|
|
1650
1725
|
// Deliberately omit handledCommentId: an infra blip (CLI crash, timeout,
|
|
1651
1726
|
// network error) never reached the agent, so it isn't an answer to the
|
|
@@ -326,6 +326,22 @@ const runLeaseSchema = z.object({
|
|
|
326
326
|
lastRenewedAt: isoTimestampSchema,
|
|
327
327
|
expiresAt: isoTimestampSchema,
|
|
328
328
|
});
|
|
329
|
+
export const failurePhaseSchema = z.enum([
|
|
330
|
+
'workspace-prep',
|
|
331
|
+
'process-starting',
|
|
332
|
+
'running',
|
|
333
|
+
'result-parsing',
|
|
334
|
+
'publishing',
|
|
335
|
+
'unknown',
|
|
336
|
+
]);
|
|
337
|
+
export const externalSideEffectsSchema = z.enum(['none', 'confirmed', 'unknown']);
|
|
338
|
+
export const retrySafetySchema = z.enum([
|
|
339
|
+
'SAFE_TO_RETRY',
|
|
340
|
+
'SAFE_TO_RESUME',
|
|
341
|
+
'REQUIRES_RECONCILIATION',
|
|
342
|
+
'MANUAL_REVIEW_REQUIRED',
|
|
343
|
+
'NOT_RETRYABLE',
|
|
344
|
+
]);
|
|
329
345
|
function legacyRunLifecycle(input) {
|
|
330
346
|
if (input.lifecycle !== undefined) {
|
|
331
347
|
return input;
|
|
@@ -369,6 +385,11 @@ export const runRecordSchema = z.preprocess((input) => {
|
|
|
369
385
|
sentinel: runnerSentinelSchema.optional(),
|
|
370
386
|
executionOutcome: executionOutcomeSchema.optional(),
|
|
371
387
|
workflowOutcome: workflowOutcomeSchema.optional(),
|
|
388
|
+
failurePhase: failurePhaseSchema.optional(),
|
|
389
|
+
processStarted: z.boolean().optional(),
|
|
390
|
+
workspaceChanged: z.boolean().optional(),
|
|
391
|
+
externalSideEffects: externalSideEffectsSchema.optional(),
|
|
392
|
+
retrySafety: retrySafetySchema.optional(),
|
|
372
393
|
summary: z.string().optional(),
|
|
373
394
|
routing: runnerRoutingSchema.optional(),
|
|
374
395
|
lease: runLeaseSchema.optional(),
|
package/dist/src/version.js
CHANGED