@the-open-engine/zeroshot 6.34.2 → 6.35.0
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/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gateway.js +1 -0
- package/lib/agent-cli-provider/adapters/gateway.js.map +1 -1
- package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -1
- package/lib/agent-cli-provider/gateway-tools.js +23 -1
- package/lib/agent-cli-provider/gateway-tools.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +2 -2
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +3 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +27 -3
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/delivery-contract.js +139 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +24 -19
- package/src/agent/agent-task-executor.js +169 -68
- package/src/agent/output-extraction.js +111 -39
- package/src/agent/pr-verification.js +19 -13
- package/src/agent/provider-terminal-failure.js +186 -0
- package/src/agent-cli-provider/adapters/gateway.ts +1 -0
- package/src/agent-cli-provider/gateway-tools.ts +41 -3
- package/src/agent-cli-provider/provider-registry.ts +3 -1
- package/src/agent-cli-provider/single-agent-runtime.ts +30 -3
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/orchestrator.js +33 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
const { getProvider } = require('../providers');
|
|
4
|
+
const { extractCliFailure } = require('./output-extraction');
|
|
5
|
+
|
|
6
|
+
function categoryForProviderFailure(error, classification) {
|
|
7
|
+
const isPermanent = classification.retryable === false;
|
|
8
|
+
const authenticationPattern =
|
|
9
|
+
/(?:invalid[_ -]?api[_ -]?key|api[_ -]?key.*invalid|unauthori[sz]ed|forbidden|authentication|permission denied)/i;
|
|
10
|
+
if (isPermanent && authenticationPattern.test(error)) return 'authentication';
|
|
11
|
+
|
|
12
|
+
const quotaPattern = /(?:insufficient[_ -]?quota|quota exceeded|resource_exhausted)/i;
|
|
13
|
+
if (isPermanent && quotaPattern.test(error)) return 'quota';
|
|
14
|
+
if (isPermanent) return 'permanent';
|
|
15
|
+
return classification.kind === 'unknown-retryable' ? 'unknown' : 'transient';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function classifyProviderFailure(providerName, error) {
|
|
19
|
+
let rawClassification = { retryable: true, kind: 'unknown-retryable' };
|
|
20
|
+
try {
|
|
21
|
+
rawClassification = getProvider(providerName).adapter.classifyError(new Error(error));
|
|
22
|
+
} catch {
|
|
23
|
+
// Extraction remains available if a provider adapter cannot be loaded.
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
retryable: rawClassification?.retryable !== false,
|
|
27
|
+
kind:
|
|
28
|
+
typeof rawClassification?.kind === 'string' ? rawClassification.kind : 'unknown-retryable',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractProviderFailure(output, providerName) {
|
|
33
|
+
const cliError = extractCliFailure(output, providerName);
|
|
34
|
+
if (!cliError) return null;
|
|
35
|
+
|
|
36
|
+
const classification = classifyProviderFailure(providerName, cliError.error);
|
|
37
|
+
const category = categoryForProviderFailure(cliError.error, classification);
|
|
38
|
+
return {
|
|
39
|
+
error: `Provider ${cliError.provider} failed (${category}; ${classification.kind})`,
|
|
40
|
+
provider: cliError.provider,
|
|
41
|
+
event: cliError.provider === 'codex' ? 'turn.failed' : 'terminal_error',
|
|
42
|
+
category,
|
|
43
|
+
classification,
|
|
44
|
+
diagnostic: cliError.diagnostic,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redactTerminalFailureForControlPlane(state, providerName, content) {
|
|
49
|
+
const failure = extractProviderFailure(content, providerName);
|
|
50
|
+
if (!failure) return content;
|
|
51
|
+
|
|
52
|
+
state.providerFailure = failure;
|
|
53
|
+
let eventType = 'provider.failure';
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(content);
|
|
56
|
+
if (typeof parsed?.type === 'string') eventType = parsed.type;
|
|
57
|
+
} catch {
|
|
58
|
+
// extractProviderFailure already proved a supported terminal envelope.
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
type: eventType,
|
|
62
|
+
...(providerName === 'claude' ? { is_error: true } : {}),
|
|
63
|
+
...(providerName === 'gemini' ? { status: 'error', severity: 'error' } : {}),
|
|
64
|
+
error: { message: failure.error },
|
|
65
|
+
zeroshot_failure: {
|
|
66
|
+
provider: failure.provider,
|
|
67
|
+
event: failure.event,
|
|
68
|
+
category: failure.category,
|
|
69
|
+
kind: failure.classification.kind,
|
|
70
|
+
retryable: failure.classification.retryable,
|
|
71
|
+
diagnostic: failure.diagnostic,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function decorateError(error, failure) {
|
|
77
|
+
if (!failure) return error;
|
|
78
|
+
error.provider = failure.provider || null;
|
|
79
|
+
error.providerEvent = failure.event || null;
|
|
80
|
+
error.providerCategory = failure.category || null;
|
|
81
|
+
error.classification = failure.classification || null;
|
|
82
|
+
error.providerDiagnostic = failure.diagnostic || null;
|
|
83
|
+
if (failure.classification?.retryable === false) error.permanent = true;
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function receiptFields(error) {
|
|
88
|
+
if (!error?.provider) return {};
|
|
89
|
+
return {
|
|
90
|
+
provider: error.provider,
|
|
91
|
+
event: error.providerEvent,
|
|
92
|
+
category: error.providerCategory,
|
|
93
|
+
kind: error.classification?.kind,
|
|
94
|
+
retryable: error.classification?.retryable,
|
|
95
|
+
diagnostic: error.providerDiagnostic,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function workerFailure(error) {
|
|
100
|
+
const authenticationFailure =
|
|
101
|
+
error?.provider &&
|
|
102
|
+
error?.classification?.retryable === false &&
|
|
103
|
+
error?.providerCategory === 'authentication';
|
|
104
|
+
return authenticationFailure
|
|
105
|
+
? { code: 'refusal', reason: 'authentication_required' }
|
|
106
|
+
: { code: 'crash', reason: 'declared_failure' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function publishCriticalFailure({
|
|
110
|
+
agent,
|
|
111
|
+
error,
|
|
112
|
+
attempts,
|
|
113
|
+
worker,
|
|
114
|
+
unsupportedCapability,
|
|
115
|
+
structuredOutputInvalid,
|
|
116
|
+
}) {
|
|
117
|
+
const specific =
|
|
118
|
+
error?.hookFailure ||
|
|
119
|
+
structuredOutputInvalid ||
|
|
120
|
+
unsupportedCapability ||
|
|
121
|
+
error?.vertexModelError ||
|
|
122
|
+
error?.terminationExhausted;
|
|
123
|
+
const critical =
|
|
124
|
+
agent.role === 'implementation' ||
|
|
125
|
+
agent.role === 'coordinator' ||
|
|
126
|
+
agent.id === 'consensus-coordinator';
|
|
127
|
+
if (!critical || specific) return worker;
|
|
128
|
+
|
|
129
|
+
agent._publish({
|
|
130
|
+
topic: 'CLUSTER_FAILED',
|
|
131
|
+
receiver: 'broadcast',
|
|
132
|
+
content: {
|
|
133
|
+
text: `Critical agent ${agent.id} exhausted its retry budget`,
|
|
134
|
+
data: {
|
|
135
|
+
reason: error?.provider ? 'provider_execution_failed' : 'critical_agent_exhausted',
|
|
136
|
+
agentId: agent.id,
|
|
137
|
+
role: agent.role,
|
|
138
|
+
attempts,
|
|
139
|
+
code: worker.code,
|
|
140
|
+
workerReason: worker.reason,
|
|
141
|
+
...receiptFields(error),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
return worker;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildFinalFailureInfo({
|
|
149
|
+
agent,
|
|
150
|
+
error,
|
|
151
|
+
attempts,
|
|
152
|
+
worker,
|
|
153
|
+
unsupportedCapability,
|
|
154
|
+
structuredOutputInvalid,
|
|
155
|
+
}) {
|
|
156
|
+
return {
|
|
157
|
+
...(error?.terminationExhausted ? agent.cluster.failureInfo : {}),
|
|
158
|
+
agentId: agent.id,
|
|
159
|
+
taskId: error?.taskId || agent.currentTaskId,
|
|
160
|
+
iteration: agent.iteration,
|
|
161
|
+
error: error.message,
|
|
162
|
+
attempts,
|
|
163
|
+
...receiptFields(error),
|
|
164
|
+
...(error?.provider ? { code: worker.code, workerReason: worker.reason } : {}),
|
|
165
|
+
...(unsupportedCapability
|
|
166
|
+
? {
|
|
167
|
+
code: error.code,
|
|
168
|
+
permanent: true,
|
|
169
|
+
provider: error.provider,
|
|
170
|
+
capability: error.capability,
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
...(structuredOutputInvalid ? { code: error.code, details: error.details ?? null } : {}),
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
buildFinalFailureInfo,
|
|
180
|
+
decorateError,
|
|
181
|
+
extractProviderFailure,
|
|
182
|
+
publishCriticalFailure,
|
|
183
|
+
receiptFields,
|
|
184
|
+
redactTerminalFailureForControlPlane,
|
|
185
|
+
workerFailure,
|
|
186
|
+
};
|
|
@@ -52,6 +52,7 @@ export function normalizeGatewayBuildOptions(
|
|
|
52
52
|
const protocol = optionalGatewayProtocol(value.protocol, `${field}.protocol`);
|
|
53
53
|
const baseUrl = optionalString(value.baseUrl, `${field}.baseUrl`);
|
|
54
54
|
const apiKey = optionalString(value.apiKey, `${field}.apiKey`);
|
|
55
|
+
const apiKeyEnv = optionalEnvironmentName(value.apiKeyEnv, `${field}.apiKeyEnv`);
|
|
55
56
|
const model = optionalNullableString(value.model, `${field}.model`);
|
|
56
57
|
const maxTokens = optionalNullablePositiveInteger(value.maxTokens, `${field}.maxTokens`);
|
|
57
58
|
const headers = optionalStringRecord(value.headers, `${field}.headers`);
|
|
@@ -60,6 +61,13 @@ export function normalizeGatewayBuildOptions(
|
|
|
60
61
|
if (protocol !== undefined) result.protocol = protocol;
|
|
61
62
|
if (baseUrl !== undefined) result.baseUrl = baseUrl;
|
|
62
63
|
if (typeof apiKey === 'string') result.apiKey = apiKey;
|
|
64
|
+
if (apiKeyEnv !== undefined) result.apiKeyEnv = apiKeyEnv;
|
|
65
|
+
if (apiKey !== undefined && apiKeyEnv !== undefined) {
|
|
66
|
+
invalidField(
|
|
67
|
+
`${field}.apiKeyEnv`,
|
|
68
|
+
`${field}.apiKey and ${field}.apiKeyEnv are mutually exclusive.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
63
71
|
if (model !== undefined) result.model = model;
|
|
64
72
|
if (maxTokens !== undefined) result.maxTokens = maxTokens;
|
|
65
73
|
if (headers !== undefined) result.headers = headers;
|
|
@@ -115,6 +123,18 @@ export function validateGatewaySettings(settings: Record<string, unknown>): stri
|
|
|
115
123
|
);
|
|
116
124
|
optionalString(settings.baseUrl, 'providerSettings.gateway.baseUrl');
|
|
117
125
|
optionalString(settings.apiKey, 'providerSettings.gateway.apiKey');
|
|
126
|
+
optionalEnvironmentName(settings.apiKeyEnv, 'providerSettings.gateway.apiKeyEnv');
|
|
127
|
+
if (
|
|
128
|
+
settings.apiKey !== null &&
|
|
129
|
+
settings.apiKey !== undefined &&
|
|
130
|
+
settings.apiKeyEnv !== null &&
|
|
131
|
+
settings.apiKeyEnv !== undefined
|
|
132
|
+
) {
|
|
133
|
+
invalidField(
|
|
134
|
+
'providerSettings.gateway.apiKeyEnv',
|
|
135
|
+
'providerSettings.gateway.apiKey and providerSettings.gateway.apiKeyEnv are mutually exclusive.'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
118
138
|
optionalNullableString(settings.model, 'providerSettings.gateway.model');
|
|
119
139
|
const maxTokens = optionalNullablePositiveInteger(
|
|
120
140
|
settings.maxTokens,
|
|
@@ -138,6 +158,14 @@ export function validateGatewaySettings(settings: Record<string, unknown>): stri
|
|
|
138
158
|
}
|
|
139
159
|
}
|
|
140
160
|
|
|
161
|
+
function optionalEnvironmentName(value: unknown, field: string): string | undefined {
|
|
162
|
+
const name = optionalString(value, field);
|
|
163
|
+
if (name !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
164
|
+
invalidField(field, `${field} must name an environment variable.`);
|
|
165
|
+
}
|
|
166
|
+
return name;
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
export function normalizeGatewayToolPolicy(
|
|
142
170
|
value: GatewayToolPolicy,
|
|
143
171
|
field: string,
|
|
@@ -212,7 +240,11 @@ function requiredGatewayToolPolicy(
|
|
|
212
240
|
return normalizeGatewayToolPolicy(value, field, cwd);
|
|
213
241
|
}
|
|
214
242
|
|
|
215
|
-
function normalizeGatewayRoots(
|
|
243
|
+
function normalizeGatewayRoots(
|
|
244
|
+
value: readonly string[],
|
|
245
|
+
field: string,
|
|
246
|
+
cwd: string
|
|
247
|
+
): readonly string[] {
|
|
216
248
|
if (value.length === 0) {
|
|
217
249
|
invalidField(field, `${field} must contain at least one root path.`);
|
|
218
250
|
}
|
|
@@ -368,7 +400,11 @@ async function readGatewayFile(
|
|
|
368
400
|
input: unknown,
|
|
369
401
|
policy: GatewayToolPolicy
|
|
370
402
|
): Promise<Record<string, unknown>> {
|
|
371
|
-
const targetPath = await assertWithinRoots(
|
|
403
|
+
const targetPath = await assertWithinRoots(
|
|
404
|
+
readGatewayFileInput(input),
|
|
405
|
+
policy.roots,
|
|
406
|
+
'read_file.path'
|
|
407
|
+
);
|
|
372
408
|
const content = await readFile(targetPath, 'utf8');
|
|
373
409
|
return { path: targetPath, content };
|
|
374
410
|
}
|
|
@@ -420,7 +456,9 @@ async function applyGatewayPatch(
|
|
|
420
456
|
if (!current.includes(search)) {
|
|
421
457
|
throw new Error('apply_patch.search did not match the target file.');
|
|
422
458
|
}
|
|
423
|
-
const next = request.replaceAll
|
|
459
|
+
const next = request.replaceAll
|
|
460
|
+
? current.split(search).join(replace)
|
|
461
|
+
: current.replace(search, replace);
|
|
424
462
|
await writeFile(targetPath, next, 'utf8');
|
|
425
463
|
return {
|
|
426
464
|
path: targetPath,
|
|
@@ -316,13 +316,15 @@ export const providerRegistry = [
|
|
|
316
316
|
invoke: SPAWN_INVOKE,
|
|
317
317
|
installInstructions: 'Bundled with Zeroshot; no external provider CLI install is required.',
|
|
318
318
|
authInstructions:
|
|
319
|
-
'Configure providerSettings.gateway
|
|
319
|
+
'Configure providerSettings.gateway protocol, base URL, apiKey or apiKeyEnv, model, ' +
|
|
320
|
+
'maxTokens, and toolPolicy in Zeroshot settings.',
|
|
320
321
|
credentialPaths: [],
|
|
321
322
|
credentialEnvKeys: gatewayAdapter.credentialEnvKeys,
|
|
322
323
|
settingsFields: [
|
|
323
324
|
'protocol',
|
|
324
325
|
'baseUrl',
|
|
325
326
|
'apiKey',
|
|
327
|
+
'apiKeyEnv',
|
|
326
328
|
'headers',
|
|
327
329
|
'model',
|
|
328
330
|
'maxTokens',
|
|
@@ -766,6 +766,7 @@ function resolveRuntimeGatewayOptions(
|
|
|
766
766
|
const cwd = baseOptions.cwd ?? process.cwd();
|
|
767
767
|
const settingsGateway = providerSettings.gateway ?? {};
|
|
768
768
|
const requestGateway = baseOptions.gateway ?? {};
|
|
769
|
+
const apiKey = resolveGatewayApiKey(requestGateway, settingsGateway);
|
|
769
770
|
const mergedHeaders =
|
|
770
771
|
requestGateway.headers === undefined
|
|
771
772
|
? settingsGateway.headers
|
|
@@ -777,9 +778,7 @@ function resolveRuntimeGatewayOptions(
|
|
|
777
778
|
...((requestGateway.baseUrl ?? settingsGateway.baseUrl)
|
|
778
779
|
? { baseUrl: requestGateway.baseUrl ?? settingsGateway.baseUrl }
|
|
779
780
|
: {}),
|
|
780
|
-
...(
|
|
781
|
-
? { apiKey: requestGateway.apiKey ?? settingsGateway.apiKey }
|
|
782
|
-
: {}),
|
|
781
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
783
782
|
...(mergedHeaders === undefined ? {} : { headers: mergedHeaders }),
|
|
784
783
|
model: requestGateway.model ?? modelSpec.model ?? settingsGateway.model ?? null,
|
|
785
784
|
...((requestGateway.maxTokens ?? settingsGateway.maxTokens)
|
|
@@ -792,6 +791,34 @@ function resolveRuntimeGatewayOptions(
|
|
|
792
791
|
return resolveGatewayConfiguration(mergedGateway, 'options.gateway', cwd);
|
|
793
792
|
}
|
|
794
793
|
|
|
794
|
+
function resolveGatewayApiKey(
|
|
795
|
+
request: GatewayBuildOptions,
|
|
796
|
+
settings: GatewayBuildOptions
|
|
797
|
+
): string | undefined {
|
|
798
|
+
if (request.apiKeyEnv !== undefined) {
|
|
799
|
+
throw new Error('options.gateway.apiKeyEnv requires trusted provider settings');
|
|
800
|
+
}
|
|
801
|
+
const requestOwnsTransport =
|
|
802
|
+
request.protocol !== undefined ||
|
|
803
|
+
request.baseUrl !== undefined ||
|
|
804
|
+
request.headers !== undefined;
|
|
805
|
+
if ((settings.apiKeyEnv !== undefined || settings.apiKey !== undefined) && requestOwnsTransport) {
|
|
806
|
+
throw new Error('options.gateway transport cannot override credential settings');
|
|
807
|
+
}
|
|
808
|
+
const direct = request.apiKey ?? settings.apiKey;
|
|
809
|
+
const environmentName = settings.apiKeyEnv;
|
|
810
|
+
if (direct !== undefined && environmentName !== undefined) {
|
|
811
|
+
throw new Error('gateway apiKey and apiKeyEnv are mutually exclusive');
|
|
812
|
+
}
|
|
813
|
+
if (direct !== undefined) return direct;
|
|
814
|
+
if (environmentName === undefined) return undefined;
|
|
815
|
+
const value = process.env[environmentName];
|
|
816
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
817
|
+
throw new Error(`gateway apiKeyEnv requires environment variable ${environmentName}`);
|
|
818
|
+
}
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
|
|
795
822
|
function shouldIncludeAuthEnv(
|
|
796
823
|
baseOptions: BuildProviderCommandOptions,
|
|
797
824
|
authEnv: Readonly<Record<string, string>>
|
|
@@ -74,6 +74,7 @@ export interface GatewayBuildOptions {
|
|
|
74
74
|
readonly protocol?: GatewayProtocol;
|
|
75
75
|
readonly baseUrl?: string;
|
|
76
76
|
readonly apiKey?: string;
|
|
77
|
+
readonly apiKeyEnv?: string;
|
|
77
78
|
readonly headers?: Readonly<Record<string, string>>;
|
|
78
79
|
readonly model?: string | null;
|
|
79
80
|
readonly maxTokens?: number;
|
package/src/orchestrator.js
CHANGED
|
@@ -275,6 +275,7 @@ class Orchestrator {
|
|
|
275
275
|
// Track if orchestrator is closed (prevents _saveClusters race conditions during cleanup)
|
|
276
276
|
this.closed = false;
|
|
277
277
|
this._conductorWatchdogs = new Set();
|
|
278
|
+
this._clusterRunBoundaries = new Map();
|
|
278
279
|
|
|
279
280
|
// Track if clusters are loaded (for lazy loading pattern)
|
|
280
281
|
this._clustersLoaded = options.skipLoad === true;
|
|
@@ -1575,11 +1576,17 @@ class Orchestrator {
|
|
|
1575
1576
|
}
|
|
1576
1577
|
|
|
1577
1578
|
_registerAgentErrorHandler(messageBus, clusterId) {
|
|
1579
|
+
this._recordClusterRunBoundary(messageBus, clusterId);
|
|
1578
1580
|
this._subscribeToClusterTopic(messageBus, clusterId, 'AGENT_ERROR', async (message) => {
|
|
1579
1581
|
const agentRole = message.content?.data?.role;
|
|
1580
1582
|
const attempts = message.content?.data?.attempts || 1;
|
|
1581
1583
|
const hookFailure = message.content?.data?.hookFailure === true;
|
|
1582
1584
|
const restartExhausted = message.content?.data?.restartExhausted === true;
|
|
1585
|
+
const durableClusterFailure = this._findCurrentRunClusterFailure(
|
|
1586
|
+
messageBus,
|
|
1587
|
+
clusterId,
|
|
1588
|
+
message.sequence
|
|
1589
|
+
);
|
|
1583
1590
|
|
|
1584
1591
|
await this._saveClusters();
|
|
1585
1592
|
|
|
@@ -1587,7 +1594,10 @@ class Orchestrator {
|
|
|
1587
1594
|
agentRole === 'implementation' ||
|
|
1588
1595
|
agentRole === 'coordinator' ||
|
|
1589
1596
|
message.sender === 'consensus-coordinator';
|
|
1590
|
-
const shouldStop =
|
|
1597
|
+
const shouldStop =
|
|
1598
|
+
!durableClusterFailure &&
|
|
1599
|
+
shouldStopForRole &&
|
|
1600
|
+
(hookFailure || restartExhausted || attempts >= 3);
|
|
1591
1601
|
|
|
1592
1602
|
if (shouldStop) {
|
|
1593
1603
|
this._log(`\n${'='.repeat(80)}`);
|
|
@@ -1610,6 +1620,25 @@ class Orchestrator {
|
|
|
1610
1620
|
});
|
|
1611
1621
|
}
|
|
1612
1622
|
|
|
1623
|
+
_recordClusterRunBoundary(messageBus, clusterId) {
|
|
1624
|
+
const latest = messageBus.findLast({ cluster_id: clusterId, orderBySequence: true });
|
|
1625
|
+
this._clusterRunBoundaries.set(clusterId, latest?.sequence ?? null);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
_findCurrentRunClusterFailure(messageBus, clusterId, throughId) {
|
|
1629
|
+
const boundary = this._clusterRunBoundaries.get(clusterId);
|
|
1630
|
+
return (
|
|
1631
|
+
messageBus.query({
|
|
1632
|
+
cluster_id: clusterId,
|
|
1633
|
+
topic: 'CLUSTER_FAILED',
|
|
1634
|
+
order: 'desc',
|
|
1635
|
+
limit: 1,
|
|
1636
|
+
...(boundary === null ? {} : { afterId: boundary }),
|
|
1637
|
+
...(throughId === undefined ? {} : { throughId }),
|
|
1638
|
+
})[0] || null
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1613
1642
|
_registerPushBlockedHandler(messageBus, clusterId) {
|
|
1614
1643
|
this._subscribeToClusterTopic(messageBus, clusterId, 'PUSH_BLOCKED', async (message) => {
|
|
1615
1644
|
const reason = message.content?.data?.blocked_reason || 'unknown';
|
|
@@ -2425,6 +2454,7 @@ class Orchestrator {
|
|
|
2425
2454
|
|
|
2426
2455
|
// Now remove from memory after persisting
|
|
2427
2456
|
this.clusters.delete(clusterId);
|
|
2457
|
+
this._clusterRunBoundaries.delete(clusterId);
|
|
2428
2458
|
}
|
|
2429
2459
|
|
|
2430
2460
|
/**
|
|
@@ -2462,6 +2492,7 @@ class Orchestrator {
|
|
|
2462
2492
|
watchdog.dispose();
|
|
2463
2493
|
}
|
|
2464
2494
|
this._conductorWatchdogs.clear();
|
|
2495
|
+
this._clusterRunBoundaries.clear();
|
|
2465
2496
|
|
|
2466
2497
|
for (const cluster of this.clusters.values()) {
|
|
2467
2498
|
if (typeof cluster.snapshotter?.stop === 'function') {
|
|
@@ -2902,6 +2933,7 @@ class Orchestrator {
|
|
|
2902
2933
|
}
|
|
2903
2934
|
|
|
2904
2935
|
async _restartClusterAgents(cluster) {
|
|
2936
|
+
this._recordClusterRunBoundary(cluster.messageBus, cluster.id);
|
|
2905
2937
|
cluster.state = 'running';
|
|
2906
2938
|
cluster.pid = process.pid;
|
|
2907
2939
|
for (const agent of cluster.agents) {
|