@karmaniverous/jeeves-meta 0.15.12 → 0.16.1
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/README.md +1 -1
- package/dist/cli/jeeves-meta/architect.md +1 -4
- package/dist/cli/jeeves-meta/index.js +235 -321
- package/dist/executor/GatewayExecutor.d.ts +26 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +235 -327
- package/dist/interfaces/MetaExecutor.d.ts +0 -2
- package/dist/prompts/architect.md +1 -4
- package/dist/queue/index.d.ts +25 -66
- package/dist/routes/metasUpdate.d.ts +1 -1
- package/dist/routes/queue.d.ts +2 -2
- package/dist/schema/config.d.ts +0 -9
- package/dist/schema/meta.d.ts +0 -3
- package/dist/seed/createMeta.d.ts +0 -6
- package/package.json +3 -3
|
@@ -782,12 +782,6 @@ const autoSeedRuleSchema = z.object({
|
|
|
782
782
|
crossRefs: z.array(z.string()).optional(),
|
|
783
783
|
/** Walk up this many extra parent levels from the matched file's directory. Default 0. */
|
|
784
784
|
parentDepth: z.number().int().min(0).optional(),
|
|
785
|
-
/** Per-category timeout override for the architect phase (seconds, min 30). */
|
|
786
|
-
architectTimeout: z.number().int().min(30).optional(),
|
|
787
|
-
/** Per-category timeout override for the builder phase (seconds, min 30). */
|
|
788
|
-
builderTimeout: z.number().int().min(30).optional(),
|
|
789
|
-
/** Per-category timeout override for the critic phase (seconds, min 30). */
|
|
790
|
-
criticTimeout: z.number().int().min(30).optional(),
|
|
791
785
|
});
|
|
792
786
|
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
793
787
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
@@ -925,7 +919,7 @@ class SpawnTimeoutError extends Error {
|
|
|
925
919
|
* @module executor/GatewayExecutor
|
|
926
920
|
*/
|
|
927
921
|
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
928
|
-
const
|
|
922
|
+
const DEFAULT_SAFETY_VALVE_MS = 3_600_000; // 1 hour fallback
|
|
929
923
|
/**
|
|
930
924
|
* MetaExecutor that spawns OpenClaw sessions via the gateway's
|
|
931
925
|
* `/tools/invoke` endpoint.
|
|
@@ -956,6 +950,46 @@ class GatewayExecutor {
|
|
|
956
950
|
/* best-effort cleanup */
|
|
957
951
|
}
|
|
958
952
|
}
|
|
953
|
+
/** Read and clean up the staging output file. Returns content or undefined if absent. */
|
|
954
|
+
readStagingFile(outputPath) {
|
|
955
|
+
if (!existsSync(outputPath))
|
|
956
|
+
return undefined;
|
|
957
|
+
try {
|
|
958
|
+
return readFileSync(outputPath, 'utf8');
|
|
959
|
+
}
|
|
960
|
+
finally {
|
|
961
|
+
this.cleanupOutputFile(outputPath);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/** Extract plain text from a message content field, skipping ANNOUNCE_SKIP sentinels. */
|
|
965
|
+
static extractMessageText(content) {
|
|
966
|
+
if (!content)
|
|
967
|
+
return undefined;
|
|
968
|
+
const text = typeof content === 'string'
|
|
969
|
+
? content
|
|
970
|
+
: Array.isArray(content)
|
|
971
|
+
? content
|
|
972
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
973
|
+
.map((b) => b.text)
|
|
974
|
+
.join('\n')
|
|
975
|
+
: '';
|
|
976
|
+
return text && text.trim() !== 'ANNOUNCE_SKIP' ? text : undefined;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Check history messages for timeout detection.
|
|
980
|
+
*
|
|
981
|
+
* Does NOT determine completion — session completion is authoritative
|
|
982
|
+
* (via sessions_list). History stop reasons can false-positive on
|
|
983
|
+
* sessions_yield artifacts (#200).
|
|
984
|
+
*/
|
|
985
|
+
static checkHistoryCompletion(messages) {
|
|
986
|
+
if (messages.length === 0)
|
|
987
|
+
return { timedOut: false };
|
|
988
|
+
const last = messages[messages.length - 1];
|
|
989
|
+
if (last.role !== 'assistant' || !last.stopReason)
|
|
990
|
+
return { timedOut: false };
|
|
991
|
+
return { timedOut: last.stopReason === 'timeout' };
|
|
992
|
+
}
|
|
959
993
|
/** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
|
|
960
994
|
async invoke(tool, args, sessionKey) {
|
|
961
995
|
const headers = {
|
|
@@ -982,7 +1016,13 @@ class GatewayExecutor {
|
|
|
982
1016
|
}
|
|
983
1017
|
return data;
|
|
984
1018
|
}
|
|
985
|
-
/**
|
|
1019
|
+
/**
|
|
1020
|
+
* Look up session metadata (tokens, completion status) via sessions_list.
|
|
1021
|
+
*
|
|
1022
|
+
* Detects gateway-side timeout (`status: "timeout"`) and killed sessions
|
|
1023
|
+
* (`status: "killed"`) as completed, with a `timedOut` flag to distinguish
|
|
1024
|
+
* timeout from normal completion.
|
|
1025
|
+
*/
|
|
986
1026
|
async getSessionInfo(sessionKey) {
|
|
987
1027
|
try {
|
|
988
1028
|
const result = await this.invoke('sessions_list', {
|
|
@@ -998,13 +1038,18 @@ class GatewayExecutor {
|
|
|
998
1038
|
// With limit=200 this is reliable; a false positive here only
|
|
999
1039
|
// means we read the output file slightly early (still correct
|
|
1000
1040
|
// if the file exists).
|
|
1001
|
-
return { completed: true };
|
|
1041
|
+
return { completed: true, timedOut: false };
|
|
1002
1042
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1043
|
+
const status = match.status;
|
|
1044
|
+
const done = status === 'completed' ||
|
|
1045
|
+
status === 'done' ||
|
|
1046
|
+
status === 'timeout' ||
|
|
1047
|
+
status === 'killed';
|
|
1048
|
+
const timedOut = status === 'timeout';
|
|
1049
|
+
return { tokens: match.totalTokens, completed: done, timedOut };
|
|
1005
1050
|
}
|
|
1006
1051
|
catch {
|
|
1007
|
-
return { completed: false };
|
|
1052
|
+
return { completed: false, timedOut: false };
|
|
1008
1053
|
}
|
|
1009
1054
|
}
|
|
1010
1055
|
/** Whether this executor has been aborted by the operator. */
|
|
@@ -1015,12 +1060,38 @@ class GatewayExecutor {
|
|
|
1015
1060
|
abort() {
|
|
1016
1061
|
this.controller.abort();
|
|
1017
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Query the gateway's configured subagent run timeout.
|
|
1065
|
+
*
|
|
1066
|
+
* Returns the value in milliseconds, or `undefined` if the query fails
|
|
1067
|
+
* or the value is absent/zero (no timeout configured).
|
|
1068
|
+
*/
|
|
1069
|
+
async queryGatewayRunTimeout() {
|
|
1070
|
+
try {
|
|
1071
|
+
const result = await this.invoke('session_status', {});
|
|
1072
|
+
const details = (result.result?.details ?? result.result ?? {});
|
|
1073
|
+
const runTimeoutSeconds = details.runTimeoutSeconds ??
|
|
1074
|
+
details.timeout;
|
|
1075
|
+
if (typeof runTimeoutSeconds === 'number' && runTimeoutSeconds > 0) {
|
|
1076
|
+
return runTimeoutSeconds * 1000;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
// Gateway unreachable or field not exposed — fall back to default
|
|
1081
|
+
}
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1018
1084
|
async spawn(task, options) {
|
|
1019
1085
|
// Fresh controller for each spawn call
|
|
1020
1086
|
this.controller = new AbortController();
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1087
|
+
// Safety-valve deadline: gateway's runTimeoutSeconds + 60s buffer,
|
|
1088
|
+
// defaulting to 1 hour if the gateway value is 0/absent/query fails.
|
|
1089
|
+
// This is a circuit breaker, not a timeout mechanism.
|
|
1090
|
+
const gatewayTimeoutMs = await this.queryGatewayRunTimeout();
|
|
1091
|
+
const safetyValveMs = gatewayTimeoutMs
|
|
1092
|
+
? gatewayTimeoutMs + 60_000
|
|
1093
|
+
: DEFAULT_SAFETY_VALVE_MS;
|
|
1094
|
+
const safetyDeadline = Date.now() + safetyValveMs;
|
|
1024
1095
|
// Ensure workspace dir exists
|
|
1025
1096
|
if (!existsSync(this.workspaceDir)) {
|
|
1026
1097
|
mkdirSync(this.workspaceDir, { recursive: true });
|
|
@@ -1042,7 +1113,6 @@ class GatewayExecutor {
|
|
|
1042
1113
|
const spawnResult = await this.invoke('sessions_spawn', {
|
|
1043
1114
|
task: taskWithOutput,
|
|
1044
1115
|
label,
|
|
1045
|
-
runTimeoutSeconds: timeoutSeconds,
|
|
1046
1116
|
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
1047
1117
|
...(options?.model ? { model: options.model } : {}),
|
|
1048
1118
|
});
|
|
@@ -1054,9 +1124,18 @@ class GatewayExecutor {
|
|
|
1054
1124
|
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
1055
1125
|
JSON.stringify(spawnResult));
|
|
1056
1126
|
}
|
|
1057
|
-
// Step 2: Poll for completion
|
|
1127
|
+
// Step 2: Poll for completion — gateway owns the subagent lifecycle.
|
|
1128
|
+
// Loop exits via: (a) completion detection, (b) abort signal,
|
|
1129
|
+
// (c) gateway-side timeout detection, or (d) safety-valve circuit breaker.
|
|
1058
1130
|
await sleepAsync(3000);
|
|
1059
|
-
while (
|
|
1131
|
+
while (true) {
|
|
1132
|
+
// Safety-valve circuit breaker
|
|
1133
|
+
if (Date.now() >= safetyDeadline) {
|
|
1134
|
+
this.cleanupOutputFile(outputPath);
|
|
1135
|
+
throw new SpawnTimeoutError('Safety-valve deadline exceeded (' +
|
|
1136
|
+
safetyValveMs.toString() +
|
|
1137
|
+
'ms) — gateway timeout may be misconfigured', outputPath);
|
|
1138
|
+
}
|
|
1060
1139
|
// Check for abort before each poll iteration
|
|
1061
1140
|
if (this.controller.signal.aborted) {
|
|
1062
1141
|
this.cleanupOutputFile(outputPath);
|
|
@@ -1072,63 +1151,51 @@ class GatewayExecutor {
|
|
|
1072
1151
|
historyResult.result?.messages ??
|
|
1073
1152
|
[];
|
|
1074
1153
|
const msgArray = messages;
|
|
1075
|
-
// Check 1:
|
|
1076
|
-
|
|
1077
|
-
if (msgArray.length > 0) {
|
|
1078
|
-
const lastMsg = msgArray[msgArray.length - 1];
|
|
1079
|
-
if (lastMsg.role === 'assistant' &&
|
|
1080
|
-
lastMsg.stopReason &&
|
|
1081
|
-
lastMsg.stopReason !== 'toolUse' &&
|
|
1082
|
-
lastMsg.stopReason !== 'error') {
|
|
1083
|
-
historyDone = true;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1154
|
+
// Check 1: history-based timeout detection (stop reason)
|
|
1155
|
+
const { timedOut: historyTimedOut } = GatewayExecutor.checkHistoryCompletion(msgArray);
|
|
1086
1156
|
// Check 2: session completion status via sessions_list
|
|
1157
|
+
// Gate on session completion only — history stop reasons can
|
|
1158
|
+
// false-positive on sessions_yield artifacts (#200).
|
|
1087
1159
|
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
1088
|
-
|
|
1160
|
+
const timedOut = sessionInfo.timedOut || historyTimedOut;
|
|
1161
|
+
if (sessionInfo.completed) {
|
|
1089
1162
|
const tokens = sessionInfo.tokens;
|
|
1090
|
-
//
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1163
|
+
// Gateway-side timeout detected — check staging file for recovery
|
|
1164
|
+
if (timedOut) {
|
|
1165
|
+
const output = this.readStagingFile(outputPath);
|
|
1166
|
+
if (output !== undefined)
|
|
1094
1167
|
return { output, tokens };
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
try {
|
|
1098
|
-
unlinkSync(outputPath);
|
|
1099
|
-
}
|
|
1100
|
-
catch {
|
|
1101
|
-
/* cleanup best-effort */
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1168
|
+
// No output or partial output — throw for _state recovery (§3.16.6)
|
|
1169
|
+
throw new SpawnTimeoutError('Gateway-side timeout detected (session status: timeout)', outputPath);
|
|
1104
1170
|
}
|
|
1171
|
+
// Normal completion — read output from file
|
|
1172
|
+
const output = this.readStagingFile(outputPath);
|
|
1173
|
+
if (output !== undefined)
|
|
1174
|
+
return { output, tokens };
|
|
1105
1175
|
// Fallback: extract from message content if file wasn't written.
|
|
1106
1176
|
// Skip ANNOUNCE_SKIP sentinel messages — the real output is in
|
|
1107
1177
|
// a preceding assistant message (the file write).
|
|
1108
1178
|
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
1109
1179
|
const msg = msgArray[i];
|
|
1110
|
-
if (msg.role === 'assistant'
|
|
1111
|
-
const text =
|
|
1112
|
-
|
|
1113
|
-
: Array.isArray(msg.content)
|
|
1114
|
-
? msg.content
|
|
1115
|
-
.filter((b) => b.type === 'text' && b.text)
|
|
1116
|
-
.map((b) => b.text)
|
|
1117
|
-
.join('\n')
|
|
1118
|
-
: '';
|
|
1119
|
-
if (text && text.trim() !== 'ANNOUNCE_SKIP')
|
|
1180
|
+
if (msg.role === 'assistant') {
|
|
1181
|
+
const text = GatewayExecutor.extractMessageText(msg.content);
|
|
1182
|
+
if (text !== undefined)
|
|
1120
1183
|
return { output: text, tokens };
|
|
1121
1184
|
}
|
|
1122
1185
|
}
|
|
1123
1186
|
return { output: '', tokens };
|
|
1124
1187
|
}
|
|
1125
1188
|
}
|
|
1126
|
-
catch {
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
// Re-throw SpawnTimeoutError and SpawnAbortedError — only swallow transient poll failures
|
|
1191
|
+
if (err instanceof SpawnTimeoutError ||
|
|
1192
|
+
err instanceof SpawnAbortedError) {
|
|
1193
|
+
throw err;
|
|
1194
|
+
}
|
|
1127
1195
|
// Transient poll failure — keep trying
|
|
1128
1196
|
}
|
|
1129
1197
|
await sleepAsync(this.pollIntervalMs);
|
|
1130
1198
|
}
|
|
1131
|
-
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
1132
1199
|
}
|
|
1133
1200
|
}
|
|
1134
1201
|
|
|
@@ -2626,7 +2693,6 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
2626
2693
|
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
2627
2694
|
const result = await executor.spawn(architectTask, {
|
|
2628
2695
|
thinking: config.thinking,
|
|
2629
|
-
timeout: currentMeta._architectTimeout ?? config.architectTimeout,
|
|
2630
2696
|
label: 'meta-architect',
|
|
2631
2697
|
});
|
|
2632
2698
|
const builderBrief = parseArchitectOutput(result.output);
|
|
@@ -2678,7 +2744,6 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
2678
2744
|
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
2679
2745
|
const result = await executor.spawn(builderTask, {
|
|
2680
2746
|
thinking: config.thinking,
|
|
2681
|
-
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
2682
2747
|
label: 'meta-builder',
|
|
2683
2748
|
});
|
|
2684
2749
|
const rawOutput = result.output;
|
|
@@ -2773,7 +2838,6 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
2773
2838
|
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
2774
2839
|
const result = await executor.spawn(criticTask, {
|
|
2775
2840
|
thinking: config.thinking,
|
|
2776
|
-
timeout: currentMeta._criticTimeout ?? config.criticTimeout,
|
|
2777
2841
|
label: 'meta-critic',
|
|
2778
2842
|
});
|
|
2779
2843
|
const feedback = parseCriticOutput(result.output);
|
|
@@ -3109,36 +3173,33 @@ class ProgressReporter {
|
|
|
3109
3173
|
}
|
|
3110
3174
|
|
|
3111
3175
|
/**
|
|
3112
|
-
*
|
|
3176
|
+
* Synthesis queue.
|
|
3113
3177
|
*
|
|
3114
3178
|
* Layer 1: Current — the single item currently executing (at most one).
|
|
3115
|
-
* Layer 2:
|
|
3116
|
-
*
|
|
3117
|
-
* Layer 3: Automatic — computed on read, not persisted. All
|
|
3118
|
-
* pending phase, ranked by scheduler priority.
|
|
3119
|
-
*
|
|
3120
|
-
* Legacy: `pending` array is the union of overrides + automatic.
|
|
3179
|
+
* Layer 2: Pending — items enqueued via POST /synthesize (targeted) or
|
|
3180
|
+
* scheduler tick (automatic). FIFO, ahead of automatic candidates.
|
|
3181
|
+
* Layer 3: Automatic — computed on read (GET /queue), not persisted. All
|
|
3182
|
+
* metas with a pending phase, ranked by scheduler priority.
|
|
3121
3183
|
*
|
|
3122
3184
|
* @module queue
|
|
3123
3185
|
*/
|
|
3124
3186
|
const DEPTH_WARNING_THRESHOLD = 3;
|
|
3187
|
+
/** Strip trailing .meta suffix for consistent path comparison. */
|
|
3188
|
+
function normQueuePath(p) {
|
|
3189
|
+
return p.endsWith('.meta') ? p.slice(0, -5).replace(/[/\\]$/, '') : p;
|
|
3190
|
+
}
|
|
3125
3191
|
/**
|
|
3126
|
-
*
|
|
3192
|
+
* Synthesis queue.
|
|
3127
3193
|
*
|
|
3128
|
-
* Only one synthesis runs at a time.
|
|
3129
|
-
* take priority over automatic candidates.
|
|
3194
|
+
* Only one synthesis runs at a time. Explicitly enqueued items
|
|
3195
|
+
* take priority over automatic (computed-on-read) candidates.
|
|
3130
3196
|
*/
|
|
3131
3197
|
class SynthesisQueue {
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
currentItem = null;
|
|
3198
|
+
entries = [];
|
|
3199
|
+
currentPhaseItem = null;
|
|
3135
3200
|
processing = false;
|
|
3136
3201
|
logger;
|
|
3137
3202
|
onEnqueueCallback = null;
|
|
3138
|
-
/** Explicit override entries (3-layer model). */
|
|
3139
|
-
overrideEntries = [];
|
|
3140
|
-
/** Currently executing item with phase info (3-layer model). */
|
|
3141
|
-
currentPhaseItem = null;
|
|
3142
3203
|
constructor(logger) {
|
|
3143
3204
|
this.logger = logger;
|
|
3144
3205
|
}
|
|
@@ -3146,52 +3207,67 @@ class SynthesisQueue {
|
|
|
3146
3207
|
onEnqueue(callback) {
|
|
3147
3208
|
this.onEnqueueCallback = callback;
|
|
3148
3209
|
}
|
|
3149
|
-
// ──
|
|
3210
|
+
// ── Enqueue / dequeue ──────────────────────────────────────────────
|
|
3150
3211
|
/**
|
|
3151
|
-
* Add an
|
|
3212
|
+
* Add an entry to the queue.
|
|
3152
3213
|
* Deduped by path. Returns position and whether already queued.
|
|
3153
3214
|
*/
|
|
3154
|
-
|
|
3215
|
+
enqueue(path) {
|
|
3216
|
+
const norm = normQueuePath(path);
|
|
3155
3217
|
// Check if currently executing
|
|
3156
|
-
if (this.currentPhaseItem
|
|
3157
|
-
this.
|
|
3218
|
+
if (this.currentPhaseItem &&
|
|
3219
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3158
3220
|
return { position: 0, alreadyQueued: true };
|
|
3159
3221
|
}
|
|
3160
|
-
// Check if already in
|
|
3161
|
-
const existing = this.
|
|
3222
|
+
// Check if already in queue
|
|
3223
|
+
const existing = this.entries.findIndex((e) => normQueuePath(e.path) === norm);
|
|
3162
3224
|
if (existing !== -1) {
|
|
3163
3225
|
return { position: existing, alreadyQueued: true };
|
|
3164
3226
|
}
|
|
3165
|
-
this.
|
|
3227
|
+
this.entries.push({
|
|
3166
3228
|
path,
|
|
3167
3229
|
enqueuedAt: new Date().toISOString(),
|
|
3168
3230
|
});
|
|
3169
|
-
const position = this.
|
|
3170
|
-
if (this.
|
|
3171
|
-
this.logger.warn({ depth: this.
|
|
3231
|
+
const position = this.entries.length - 1;
|
|
3232
|
+
if (this.entries.length > DEPTH_WARNING_THRESHOLD) {
|
|
3233
|
+
this.logger.warn({ depth: this.entries.length }, 'Queue depth exceeds threshold');
|
|
3172
3234
|
}
|
|
3173
3235
|
this.onEnqueueCallback?.();
|
|
3174
3236
|
return { position, alreadyQueued: false };
|
|
3175
3237
|
}
|
|
3176
|
-
/** Dequeue the next
|
|
3177
|
-
|
|
3178
|
-
return this.
|
|
3238
|
+
/** Dequeue the next entry, or undefined if empty. */
|
|
3239
|
+
dequeue() {
|
|
3240
|
+
return this.entries.shift();
|
|
3179
3241
|
}
|
|
3180
|
-
/** Get all
|
|
3181
|
-
get
|
|
3182
|
-
return [...this.
|
|
3242
|
+
/** Get all queued entries (shallow copy). */
|
|
3243
|
+
get items() {
|
|
3244
|
+
return [...this.entries];
|
|
3183
3245
|
}
|
|
3184
|
-
/**
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3246
|
+
/** Number of items waiting in the queue (excludes current). */
|
|
3247
|
+
get depth() {
|
|
3248
|
+
return this.entries.length;
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Remove all pending items from the queue.
|
|
3252
|
+
* Does not affect the currently-running item.
|
|
3253
|
+
*
|
|
3254
|
+
* @returns The number of items removed.
|
|
3255
|
+
*/
|
|
3256
|
+
clear() {
|
|
3257
|
+
const count = this.entries.length;
|
|
3258
|
+
this.entries = [];
|
|
3188
3259
|
return count;
|
|
3189
3260
|
}
|
|
3190
|
-
/** Check
|
|
3191
|
-
|
|
3192
|
-
|
|
3261
|
+
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3262
|
+
has(path) {
|
|
3263
|
+
const norm = normQueuePath(path);
|
|
3264
|
+
if (this.currentPhaseItem &&
|
|
3265
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3266
|
+
return true;
|
|
3267
|
+
}
|
|
3268
|
+
return this.entries.some((e) => normQueuePath(e.path) === norm);
|
|
3193
3269
|
}
|
|
3194
|
-
// ── Current-item tracking
|
|
3270
|
+
// ── Current-item tracking ──────────────────────────────────────────
|
|
3195
3271
|
/** Set the currently executing phase item. */
|
|
3196
3272
|
setCurrentPhase(path, phase) {
|
|
3197
3273
|
this.currentPhaseItem = {
|
|
@@ -3208,129 +3284,21 @@ class SynthesisQueue {
|
|
|
3208
3284
|
get currentPhase() {
|
|
3209
3285
|
return this.currentPhaseItem;
|
|
3210
3286
|
}
|
|
3211
|
-
// ──
|
|
3212
|
-
/**
|
|
3213
|
-
* Add a path to the synthesis queue.
|
|
3214
|
-
*
|
|
3215
|
-
* @param path - Meta path to synthesize.
|
|
3216
|
-
* @param priority - If true, insert at front of queue.
|
|
3217
|
-
* @returns Position and whether the path was already queued.
|
|
3218
|
-
*/
|
|
3219
|
-
enqueue(path, priority = false) {
|
|
3220
|
-
if (this.currentItem?.path === path) {
|
|
3221
|
-
return { position: 0, alreadyQueued: true };
|
|
3222
|
-
}
|
|
3223
|
-
const existingIndex = this.queue.findIndex((item) => item.path === path);
|
|
3224
|
-
if (existingIndex !== -1) {
|
|
3225
|
-
return { position: existingIndex, alreadyQueued: true };
|
|
3226
|
-
}
|
|
3227
|
-
const item = {
|
|
3228
|
-
path,
|
|
3229
|
-
priority,
|
|
3230
|
-
enqueuedAt: new Date().toISOString(),
|
|
3231
|
-
};
|
|
3232
|
-
if (priority) {
|
|
3233
|
-
this.queue.unshift(item);
|
|
3234
|
-
}
|
|
3235
|
-
else {
|
|
3236
|
-
this.queue.push(item);
|
|
3237
|
-
}
|
|
3238
|
-
if (this.queue.length > DEPTH_WARNING_THRESHOLD) {
|
|
3239
|
-
this.logger.warn({ depth: this.queue.length }, 'Queue depth exceeds threshold');
|
|
3240
|
-
}
|
|
3241
|
-
const position = this.queue.findIndex((i) => i.path === path);
|
|
3242
|
-
this.onEnqueueCallback?.();
|
|
3243
|
-
return { position, alreadyQueued: false };
|
|
3244
|
-
}
|
|
3245
|
-
/** Remove and return the next item from the queue. */
|
|
3246
|
-
dequeue() {
|
|
3247
|
-
const item = this.queue.shift();
|
|
3248
|
-
if (item) {
|
|
3249
|
-
this.currentItem = item;
|
|
3250
|
-
}
|
|
3251
|
-
return item;
|
|
3252
|
-
}
|
|
3253
|
-
/** Mark the currently-running synthesis as complete. */
|
|
3254
|
-
complete() {
|
|
3255
|
-
this.currentItem = null;
|
|
3256
|
-
}
|
|
3257
|
-
/** Number of items waiting in the queue (excludes current). */
|
|
3258
|
-
get depth() {
|
|
3259
|
-
return this.queue.length;
|
|
3260
|
-
}
|
|
3261
|
-
/** The item currently being synthesized, or null. */
|
|
3262
|
-
get current() {
|
|
3263
|
-
return this.currentItem;
|
|
3264
|
-
}
|
|
3265
|
-
/** A shallow copy of the queued items. */
|
|
3266
|
-
get items() {
|
|
3267
|
-
return [...this.queue];
|
|
3268
|
-
}
|
|
3269
|
-
/** A shallow copy of the pending items (alias for items). */
|
|
3270
|
-
get pending() {
|
|
3271
|
-
return [...this.queue];
|
|
3272
|
-
}
|
|
3273
|
-
/**
|
|
3274
|
-
* Remove all pending items from the queue.
|
|
3275
|
-
* Does not affect the currently-running item.
|
|
3276
|
-
*
|
|
3277
|
-
* @returns The number of items removed.
|
|
3278
|
-
*/
|
|
3279
|
-
clear() {
|
|
3280
|
-
const count = this.queue.length;
|
|
3281
|
-
this.queue = [];
|
|
3282
|
-
return count;
|
|
3283
|
-
}
|
|
3284
|
-
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3285
|
-
has(path) {
|
|
3286
|
-
if (this.currentItem?.path === path)
|
|
3287
|
-
return true;
|
|
3288
|
-
if (this.currentPhaseItem?.path === path)
|
|
3289
|
-
return true;
|
|
3290
|
-
return (this.queue.some((item) => item.path === path) ||
|
|
3291
|
-
this.overrideEntries.some((e) => e.path === path));
|
|
3292
|
-
}
|
|
3293
|
-
/** Get the 0-indexed position of a path in the queue. */
|
|
3294
|
-
getPosition(path) {
|
|
3295
|
-
// Check overrides first
|
|
3296
|
-
const overrideIdx = this.overrideEntries.findIndex((e) => e.path === path);
|
|
3297
|
-
if (overrideIdx !== -1)
|
|
3298
|
-
return overrideIdx;
|
|
3299
|
-
const index = this.queue.findIndex((item) => item.path === path);
|
|
3300
|
-
return index === -1 ? null : index;
|
|
3301
|
-
}
|
|
3302
|
-
/** Dequeue the next item: overrides first, then legacy queue. */
|
|
3303
|
-
nextItem() {
|
|
3304
|
-
const override = this.dequeueOverride();
|
|
3305
|
-
if (override)
|
|
3306
|
-
return { path: override.path, source: 'override' };
|
|
3307
|
-
const item = this.dequeue();
|
|
3308
|
-
if (item)
|
|
3309
|
-
return { path: item.path, source: 'legacy' };
|
|
3310
|
-
return undefined;
|
|
3311
|
-
}
|
|
3287
|
+
// ── Queue state ────────────────────────────────────────────────────
|
|
3312
3288
|
/** Return a snapshot of queue state for the /status endpoint. */
|
|
3313
3289
|
getState() {
|
|
3314
3290
|
return {
|
|
3315
|
-
depth: this.
|
|
3316
|
-
items:
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
enqueuedAt: e.enqueuedAt,
|
|
3321
|
-
})),
|
|
3322
|
-
...this.queue.map((item) => ({
|
|
3323
|
-
path: item.path,
|
|
3324
|
-
priority: item.priority,
|
|
3325
|
-
enqueuedAt: item.enqueuedAt,
|
|
3326
|
-
})),
|
|
3327
|
-
],
|
|
3291
|
+
depth: this.entries.length,
|
|
3292
|
+
items: this.entries.map((e) => ({
|
|
3293
|
+
path: e.path,
|
|
3294
|
+
enqueuedAt: e.enqueuedAt,
|
|
3295
|
+
})),
|
|
3328
3296
|
};
|
|
3329
3297
|
}
|
|
3298
|
+
// ── Processing ─────────────────────────────────────────────────────
|
|
3330
3299
|
/**
|
|
3331
|
-
* Process queued items one at a time until
|
|
3300
|
+
* Process queued items one at a time until the queue is empty.
|
|
3332
3301
|
*
|
|
3333
|
-
* Override entries are processed first (FIFO), then legacy queue items.
|
|
3334
3302
|
* Re-entry is prevented: if already processing, the call returns
|
|
3335
3303
|
* immediately. Errors are logged and do not block subsequent items.
|
|
3336
3304
|
*
|
|
@@ -3341,7 +3309,7 @@ class SynthesisQueue {
|
|
|
3341
3309
|
return;
|
|
3342
3310
|
this.processing = true;
|
|
3343
3311
|
try {
|
|
3344
|
-
let next = this.
|
|
3312
|
+
let next = this.dequeue();
|
|
3345
3313
|
while (next) {
|
|
3346
3314
|
try {
|
|
3347
3315
|
await synthesizeFn(next.path);
|
|
@@ -3350,9 +3318,7 @@ class SynthesisQueue {
|
|
|
3350
3318
|
this.logger.error({ path: next.path, err }, 'Synthesis failed');
|
|
3351
3319
|
}
|
|
3352
3320
|
this.clearCurrentPhase();
|
|
3353
|
-
|
|
3354
|
-
this.complete();
|
|
3355
|
-
next = this.nextItem();
|
|
3321
|
+
next = this.dequeue();
|
|
3356
3322
|
}
|
|
3357
3323
|
}
|
|
3358
3324
|
finally {
|
|
@@ -3686,12 +3652,6 @@ async function createMeta(ownerPath, options) {
|
|
|
3686
3652
|
metaJson._crossRefs = options.crossRefs;
|
|
3687
3653
|
if (options?.steer !== undefined)
|
|
3688
3654
|
metaJson._steer = options.steer;
|
|
3689
|
-
if (options?.architectTimeout !== undefined)
|
|
3690
|
-
metaJson._architectTimeout = options.architectTimeout;
|
|
3691
|
-
if (options?.builderTimeout !== undefined)
|
|
3692
|
-
metaJson._builderTimeout = options.builderTimeout;
|
|
3693
|
-
if (options?.criticTimeout !== undefined)
|
|
3694
|
-
metaJson._criticTimeout = options.criticTimeout;
|
|
3695
3655
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
3696
3656
|
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
3697
3657
|
return { metaDir, _id };
|
|
@@ -3767,9 +3727,6 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
3767
3727
|
candidates.set(dir, {
|
|
3768
3728
|
steer: rule.steer,
|
|
3769
3729
|
crossRefs: rule.crossRefs,
|
|
3770
|
-
architectTimeout: rule.architectTimeout,
|
|
3771
|
-
builderTimeout: rule.builderTimeout,
|
|
3772
|
-
criticTimeout: rule.criticTimeout,
|
|
3773
3730
|
});
|
|
3774
3731
|
}
|
|
3775
3732
|
}
|
|
@@ -3915,7 +3872,6 @@ class Scheduler {
|
|
|
3915
3872
|
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
|
|
3916
3873
|
return;
|
|
3917
3874
|
}
|
|
3918
|
-
// Enqueue using the legacy queue path (backward compat with processQueue)
|
|
3919
3875
|
this.queue.enqueue(candidate.path);
|
|
3920
3876
|
this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
|
|
3921
3877
|
// Opportunistic watcher restart detection
|
|
@@ -4339,7 +4295,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
4339
4295
|
/**
|
|
4340
4296
|
* PATCH /metas/:path — update user-settable reserved properties on a meta.
|
|
4341
4297
|
*
|
|
4342
|
-
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled
|
|
4298
|
+
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled.
|
|
4343
4299
|
* Set a field to null to remove it. Unknown keys are rejected.
|
|
4344
4300
|
*
|
|
4345
4301
|
* @module routes/metasUpdate
|
|
@@ -4351,9 +4307,6 @@ const updateBodySchema = z
|
|
|
4351
4307
|
_depth: z.union([z.number(), z.null()]).optional(),
|
|
4352
4308
|
_crossRefs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
4353
4309
|
_disabled: z.union([z.boolean(), z.null()]).optional(),
|
|
4354
|
-
_architectTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
4355
|
-
_builderTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
4356
|
-
_criticTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
4357
4310
|
})
|
|
4358
4311
|
.strict();
|
|
4359
4312
|
function registerMetasUpdateRoute(app, deps) {
|
|
@@ -4517,8 +4470,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
4517
4470
|
/**
|
|
4518
4471
|
* Queue management and abort routes.
|
|
4519
4472
|
*
|
|
4520
|
-
* - GET /queue — 3-layer queue model (current,
|
|
4521
|
-
* - POST /queue/clear — remove
|
|
4473
|
+
* - GET /queue — 3-layer queue model (current, pending, automatic)
|
|
4474
|
+
* - POST /queue/clear — remove pending entries
|
|
4522
4475
|
* - POST /synthesize/abort — abort the current synthesis
|
|
4523
4476
|
*
|
|
4524
4477
|
* @module routes/queue
|
|
@@ -4528,24 +4481,24 @@ function registerQueueRoutes(app, deps) {
|
|
|
4528
4481
|
const { queue } = deps;
|
|
4529
4482
|
app.get(getEndpoint('queue').path, async () => {
|
|
4530
4483
|
const currentPhase = queue.currentPhase;
|
|
4531
|
-
const
|
|
4532
|
-
// Compute owedPhase for each
|
|
4533
|
-
const
|
|
4484
|
+
const pending = queue.items;
|
|
4485
|
+
// Compute owedPhase for each pending entry by reading meta state
|
|
4486
|
+
const enrichedPending = await Promise.all(pending.map(async (entry) => {
|
|
4534
4487
|
try {
|
|
4535
|
-
const metaDir = resolveMetaDir(
|
|
4488
|
+
const metaDir = resolveMetaDir(entry.path);
|
|
4536
4489
|
const meta = await readMetaJson(metaDir);
|
|
4537
4490
|
const ps = derivePhaseState(meta);
|
|
4538
4491
|
return {
|
|
4539
|
-
path:
|
|
4492
|
+
path: entry.path,
|
|
4540
4493
|
owedPhase: getOwedPhase(ps),
|
|
4541
|
-
enqueuedAt:
|
|
4494
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4542
4495
|
};
|
|
4543
4496
|
}
|
|
4544
4497
|
catch {
|
|
4545
4498
|
return {
|
|
4546
|
-
path:
|
|
4499
|
+
path: entry.path,
|
|
4547
4500
|
owedPhase: null,
|
|
4548
|
-
enqueuedAt:
|
|
4501
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4549
4502
|
};
|
|
4550
4503
|
}
|
|
4551
4504
|
}));
|
|
@@ -4566,21 +4519,6 @@ function registerQueueRoutes(app, deps) {
|
|
|
4566
4519
|
catch {
|
|
4567
4520
|
// If listing fails, automatic stays empty
|
|
4568
4521
|
}
|
|
4569
|
-
// Legacy: pending is the union of overrides + automatic + legacy queue items
|
|
4570
|
-
const pendingItems = [
|
|
4571
|
-
...enrichedOverrides.map((o) => ({
|
|
4572
|
-
path: o.path,
|
|
4573
|
-
owedPhase: o.owedPhase,
|
|
4574
|
-
})),
|
|
4575
|
-
...automatic.map((a) => ({
|
|
4576
|
-
path: a.path,
|
|
4577
|
-
owedPhase: a.owedPhase,
|
|
4578
|
-
})),
|
|
4579
|
-
...queue.pending.map((item) => ({
|
|
4580
|
-
path: item.path,
|
|
4581
|
-
owedPhase: null,
|
|
4582
|
-
})),
|
|
4583
|
-
];
|
|
4584
4522
|
return {
|
|
4585
4523
|
current: currentPhase
|
|
4586
4524
|
? {
|
|
@@ -4588,58 +4526,45 @@ function registerQueueRoutes(app, deps) {
|
|
|
4588
4526
|
phase: currentPhase.phase,
|
|
4589
4527
|
startedAt: currentPhase.startedAt,
|
|
4590
4528
|
}
|
|
4591
|
-
:
|
|
4592
|
-
|
|
4593
|
-
path: queue.current.path,
|
|
4594
|
-
phase: null,
|
|
4595
|
-
startedAt: queue.current.enqueuedAt,
|
|
4596
|
-
}
|
|
4597
|
-
: null,
|
|
4598
|
-
overrides: enrichedOverrides,
|
|
4529
|
+
: null,
|
|
4530
|
+
pending: enrichedPending,
|
|
4599
4531
|
automatic,
|
|
4600
|
-
pending: pendingItems,
|
|
4601
|
-
// Legacy state
|
|
4602
|
-
state: queue.getState(),
|
|
4603
4532
|
};
|
|
4604
4533
|
});
|
|
4605
4534
|
app.post(getEndpoint('queueClear').path, () => {
|
|
4606
|
-
const removed = queue.
|
|
4535
|
+
const removed = queue.clear();
|
|
4607
4536
|
return { cleared: removed };
|
|
4608
4537
|
});
|
|
4609
4538
|
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
4610
|
-
// Check 3-layer current first
|
|
4611
4539
|
const currentPhase = queue.currentPhase;
|
|
4612
|
-
|
|
4613
|
-
if (!current) {
|
|
4540
|
+
if (!currentPhase) {
|
|
4614
4541
|
return reply.status(200).send({ status: 'idle' });
|
|
4615
4542
|
}
|
|
4616
4543
|
// Abort the executor
|
|
4617
4544
|
deps.executor?.abort();
|
|
4618
|
-
const metaDir = resolveMetaDir(
|
|
4619
|
-
const phase = currentPhase
|
|
4545
|
+
const metaDir = resolveMetaDir(currentPhase.path);
|
|
4546
|
+
const { phase } = currentPhase;
|
|
4620
4547
|
// Transition running phase to failed and write _error to meta.json
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
// Best-effort — meta may be unreadable
|
|
4642
|
-
}
|
|
4548
|
+
try {
|
|
4549
|
+
const meta = await readMetaJson(metaDir);
|
|
4550
|
+
let ps = derivePhaseState(meta);
|
|
4551
|
+
ps = phaseFailed(ps, phase);
|
|
4552
|
+
const updated = {
|
|
4553
|
+
...meta,
|
|
4554
|
+
_phaseState: ps,
|
|
4555
|
+
_error: {
|
|
4556
|
+
step: phase,
|
|
4557
|
+
code: 'ABORT',
|
|
4558
|
+
message: 'Aborted by operator',
|
|
4559
|
+
},
|
|
4560
|
+
};
|
|
4561
|
+
const lockPath = join(metaDir, '.lock');
|
|
4562
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
4563
|
+
await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4564
|
+
await copyFile(lockPath, metaJsonPath);
|
|
4565
|
+
}
|
|
4566
|
+
catch {
|
|
4567
|
+
// Best-effort — meta may be unreadable
|
|
4643
4568
|
}
|
|
4644
4569
|
// Release the lock for the current meta path
|
|
4645
4570
|
try {
|
|
@@ -4648,11 +4573,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
4648
4573
|
catch {
|
|
4649
4574
|
// Lock may already be released
|
|
4650
4575
|
}
|
|
4651
|
-
deps.logger.info({ path:
|
|
4576
|
+
deps.logger.info({ path: currentPhase.path }, 'Synthesis aborted');
|
|
4652
4577
|
return {
|
|
4653
4578
|
status: 'aborted',
|
|
4654
|
-
path:
|
|
4655
|
-
|
|
4579
|
+
path: currentPhase.path,
|
|
4580
|
+
phase,
|
|
4656
4581
|
};
|
|
4657
4582
|
});
|
|
4658
4583
|
}
|
|
@@ -4763,9 +4688,9 @@ async function checkWatcher(url) {
|
|
|
4763
4688
|
function deriveServiceState(deps) {
|
|
4764
4689
|
if (deps.shuttingDown)
|
|
4765
4690
|
return 'stopping';
|
|
4766
|
-
if (deps.queue.
|
|
4691
|
+
if (deps.queue.currentPhase)
|
|
4767
4692
|
return 'synthesizing';
|
|
4768
|
-
if (deps.queue.depth > 0
|
|
4693
|
+
if (deps.queue.depth > 0)
|
|
4769
4694
|
return 'waiting';
|
|
4770
4695
|
return 'idle';
|
|
4771
4696
|
}
|
|
@@ -4817,7 +4742,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4817
4742
|
}
|
|
4818
4743
|
return {
|
|
4819
4744
|
serviceState: deriveServiceState(deps),
|
|
4820
|
-
currentTarget: queue.
|
|
4745
|
+
currentTarget: queue.currentPhase?.path ?? null,
|
|
4821
4746
|
queue: queue.getState(),
|
|
4822
4747
|
stats: {
|
|
4823
4748
|
totalSyntheses: stats.totalSyntheses,
|
|
@@ -4905,7 +4830,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
4905
4830
|
alreadyQueued: false,
|
|
4906
4831
|
});
|
|
4907
4832
|
}
|
|
4908
|
-
const result = queue.
|
|
4833
|
+
const result = queue.enqueue(targetPath);
|
|
4909
4834
|
return reply.code(202).send({
|
|
4910
4835
|
status: 'queued',
|
|
4911
4836
|
path: targetPath,
|
|
@@ -4936,8 +4861,9 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
4936
4861
|
const stalest = winner.node.metaPath;
|
|
4937
4862
|
const enqueueResult = queue.enqueue(stalest);
|
|
4938
4863
|
return reply.code(202).send({
|
|
4939
|
-
status: '
|
|
4864
|
+
status: 'queued',
|
|
4940
4865
|
path: stalest,
|
|
4866
|
+
owedPhase: winner.owedPhase,
|
|
4941
4867
|
queuePosition: enqueueResult.position,
|
|
4942
4868
|
alreadyQueued: enqueueResult.alreadyQueued,
|
|
4943
4869
|
});
|
|
@@ -5086,26 +5012,14 @@ function registerShutdownHandlers(deps) {
|
|
|
5086
5012
|
deps.logger.info('Scheduler stopped');
|
|
5087
5013
|
}
|
|
5088
5014
|
// 2. Release lock for in-progress synthesis
|
|
5089
|
-
const current = deps.queue.current;
|
|
5090
|
-
if (current) {
|
|
5091
|
-
try {
|
|
5092
|
-
releaseLock(current.path);
|
|
5093
|
-
deps.logger.info({ path: current.path }, 'Released lock for in-progress synthesis');
|
|
5094
|
-
}
|
|
5095
|
-
catch {
|
|
5096
|
-
deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
|
|
5097
|
-
}
|
|
5098
|
-
}
|
|
5099
|
-
// Release lock for in-progress override synthesis (only when it
|
|
5100
|
-
// differs from the legacy current item to avoid double-release)
|
|
5101
5015
|
const currentPhase = deps.queue.currentPhase;
|
|
5102
|
-
if (currentPhase
|
|
5016
|
+
if (currentPhase) {
|
|
5103
5017
|
try {
|
|
5104
5018
|
releaseLock(resolveMetaDir(currentPhase.path));
|
|
5105
|
-
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress
|
|
5019
|
+
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress synthesis');
|
|
5106
5020
|
}
|
|
5107
5021
|
catch {
|
|
5108
|
-
deps.logger.warn({ path: currentPhase.path }, 'Failed to release
|
|
5022
|
+
deps.logger.warn({ path: currentPhase.path }, 'Failed to release lock during shutdown');
|
|
5109
5023
|
}
|
|
5110
5024
|
}
|
|
5111
5025
|
// 3. Close server
|