@femtomc/mu-server 26.2.56 → 26.2.57
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/control_plane.d.ts +4 -1
- package/dist/control_plane.js +2 -0
- package/dist/cron_programs.d.ts +122 -0
- package/dist/cron_programs.js +536 -0
- package/dist/cron_schedule.d.ts +19 -0
- package/dist/cron_schedule.js +383 -0
- package/dist/cron_timer.d.ts +21 -0
- package/dist/cron_timer.js +109 -0
- package/dist/heartbeat_programs.d.ts +6 -1
- package/dist/heartbeat_programs.js +70 -77
- package/dist/heartbeat_scheduler.js +94 -51
- package/dist/index.d.ts +11 -5
- package/dist/index.js +5 -2
- package/dist/run_supervisor.d.ts +2 -0
- package/dist/run_supervisor.js +28 -3
- package/dist/server.d.ts +4 -0
- package/dist/server.js +553 -1
- package/package.json +6 -6
|
@@ -34,6 +34,13 @@ function normalizeTarget(input) {
|
|
|
34
34
|
}
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
+
function normalizeWakeMode(value) {
|
|
38
|
+
if (typeof value !== "string") {
|
|
39
|
+
return "immediate";
|
|
40
|
+
}
|
|
41
|
+
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
42
|
+
return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
|
|
43
|
+
}
|
|
37
44
|
function sanitizeMetadata(value) {
|
|
38
45
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
39
46
|
return {};
|
|
@@ -52,9 +59,7 @@ function normalizeProgram(row) {
|
|
|
52
59
|
return null;
|
|
53
60
|
}
|
|
54
61
|
const everyMsRaw = record.every_ms;
|
|
55
|
-
const everyMs = typeof everyMsRaw === "number" && Number.isFinite(everyMsRaw)
|
|
56
|
-
? Math.max(0, Math.trunc(everyMsRaw))
|
|
57
|
-
: 0;
|
|
62
|
+
const everyMs = typeof everyMsRaw === "number" && Number.isFinite(everyMsRaw) ? Math.max(0, Math.trunc(everyMsRaw)) : 0;
|
|
58
63
|
const createdAt = typeof record.created_at_ms === "number" && Number.isFinite(record.created_at_ms)
|
|
59
64
|
? Math.trunc(record.created_at_ms)
|
|
60
65
|
: defaultNowMs();
|
|
@@ -72,6 +77,7 @@ function normalizeProgram(row) {
|
|
|
72
77
|
? lastResultRaw
|
|
73
78
|
: null;
|
|
74
79
|
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : "scheduled";
|
|
80
|
+
const wakeMode = normalizeWakeMode(record.wake_mode);
|
|
75
81
|
return {
|
|
76
82
|
v: 1,
|
|
77
83
|
program_id: programId,
|
|
@@ -79,6 +85,7 @@ function normalizeProgram(row) {
|
|
|
79
85
|
enabled: record.enabled !== false,
|
|
80
86
|
every_ms: everyMs,
|
|
81
87
|
reason,
|
|
88
|
+
wake_mode: wakeMode,
|
|
82
89
|
target,
|
|
83
90
|
metadata: sanitizeMetadata(record.metadata),
|
|
84
91
|
created_at_ms: createdAt,
|
|
@@ -180,12 +187,17 @@ export class HeartbeatProgramRegistry {
|
|
|
180
187
|
const nowMs = Math.trunc(this.#nowMs());
|
|
181
188
|
program.last_triggered_at_ms = nowMs;
|
|
182
189
|
program.updated_at_ms = nowMs;
|
|
190
|
+
let heartbeatResult;
|
|
191
|
+
let eventStatus = "ok";
|
|
192
|
+
let eventReason = heartbeatReason;
|
|
193
|
+
let eventMessage = `heartbeat program tick: ${program.title}`;
|
|
183
194
|
try {
|
|
184
195
|
const result = program.target.kind === "run"
|
|
185
196
|
? await this.#runHeartbeat({
|
|
186
197
|
jobId: program.target.job_id,
|
|
187
198
|
rootIssueId: program.target.root_issue_id,
|
|
188
199
|
reason: heartbeatReason,
|
|
200
|
+
wakeMode: program.wake_mode,
|
|
189
201
|
})
|
|
190
202
|
: await this.#activityHeartbeat({
|
|
191
203
|
activityId: program.target.activity_id,
|
|
@@ -194,92 +206,69 @@ export class HeartbeatProgramRegistry {
|
|
|
194
206
|
if (result.ok) {
|
|
195
207
|
program.last_result = "ok";
|
|
196
208
|
program.last_error = null;
|
|
197
|
-
|
|
198
|
-
// Best effort persistence on background ticks.
|
|
199
|
-
});
|
|
200
|
-
void this.#emitTickEvent({
|
|
201
|
-
ts_ms: nowMs,
|
|
202
|
-
program_id: program.program_id,
|
|
203
|
-
message: `heartbeat program tick: ${program.title}`,
|
|
204
|
-
status: "ok",
|
|
205
|
-
reason: heartbeatReason,
|
|
206
|
-
program: this.#snapshot(program),
|
|
207
|
-
}).catch(() => {
|
|
208
|
-
// best effort only
|
|
209
|
-
});
|
|
210
|
-
return { status: "ran" };
|
|
209
|
+
heartbeatResult = { status: "ran" };
|
|
211
210
|
}
|
|
212
|
-
if (result.reason === "not_running") {
|
|
211
|
+
else if (result.reason === "not_running") {
|
|
213
212
|
program.last_result = "not_running";
|
|
214
213
|
program.last_error = null;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
ts_ms: nowMs,
|
|
220
|
-
program_id: program.program_id,
|
|
221
|
-
message: `heartbeat program skipped (not running): ${program.title}`,
|
|
222
|
-
status: "not_running",
|
|
223
|
-
reason: result.reason,
|
|
224
|
-
program: this.#snapshot(program),
|
|
225
|
-
}).catch(() => {
|
|
226
|
-
// best effort only
|
|
227
|
-
});
|
|
228
|
-
return { status: "skipped", reason: "not_running" };
|
|
214
|
+
eventStatus = "not_running";
|
|
215
|
+
eventReason = result.reason;
|
|
216
|
+
eventMessage = `heartbeat program skipped (not running): ${program.title}`;
|
|
217
|
+
heartbeatResult = { status: "skipped", reason: "not_running" };
|
|
229
218
|
}
|
|
230
|
-
if (result.reason === "not_found") {
|
|
219
|
+
else if (result.reason === "not_found") {
|
|
231
220
|
program.last_result = "not_found";
|
|
232
221
|
program.last_error = null;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
});
|
|
246
|
-
return { status: "skipped", reason: "not_found" };
|
|
222
|
+
eventStatus = "not_found";
|
|
223
|
+
eventReason = result.reason;
|
|
224
|
+
eventMessage = `heartbeat program skipped (not found): ${program.title}`;
|
|
225
|
+
heartbeatResult = { status: "skipped", reason: "not_found" };
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
program.last_result = "failed";
|
|
229
|
+
program.last_error = result.reason ?? "heartbeat_program_tick_failed";
|
|
230
|
+
eventStatus = "failed";
|
|
231
|
+
eventReason = program.last_error;
|
|
232
|
+
eventMessage = `heartbeat program failed: ${program.title}`;
|
|
233
|
+
heartbeatResult = { status: "failed", reason: program.last_error };
|
|
247
234
|
}
|
|
248
|
-
program.last_result = "failed";
|
|
249
|
-
program.last_error = result.reason ?? "heartbeat_program_tick_failed";
|
|
250
|
-
void this.#persist().catch(() => {
|
|
251
|
-
// Best effort persistence on background ticks.
|
|
252
|
-
});
|
|
253
|
-
void this.#emitTickEvent({
|
|
254
|
-
ts_ms: nowMs,
|
|
255
|
-
program_id: program.program_id,
|
|
256
|
-
message: `heartbeat program failed: ${program.title}`,
|
|
257
|
-
status: "failed",
|
|
258
|
-
reason: program.last_error,
|
|
259
|
-
program: this.#snapshot(program),
|
|
260
|
-
}).catch(() => {
|
|
261
|
-
// best effort only
|
|
262
|
-
});
|
|
263
|
-
return { status: "failed", reason: program.last_error };
|
|
264
235
|
}
|
|
265
236
|
catch (err) {
|
|
266
237
|
program.last_result = "failed";
|
|
267
238
|
program.last_error = err instanceof Error ? err.message : String(err);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
239
|
+
eventStatus = "failed";
|
|
240
|
+
eventReason = program.last_error;
|
|
241
|
+
eventMessage = `heartbeat program failed: ${program.title}`;
|
|
242
|
+
heartbeatResult = { status: "failed", reason: program.last_error };
|
|
243
|
+
}
|
|
244
|
+
const shouldAutoDisableOnTerminal = program.target.kind === "run" &&
|
|
245
|
+
program.metadata.auto_disable_on_terminal === true &&
|
|
246
|
+
heartbeatResult.status === "skipped" &&
|
|
247
|
+
(heartbeatResult.reason === "not_running" || heartbeatResult.reason === "not_found");
|
|
248
|
+
if (shouldAutoDisableOnTerminal) {
|
|
249
|
+
program.enabled = false;
|
|
250
|
+
program.every_ms = 0;
|
|
251
|
+
program.updated_at_ms = Math.trunc(this.#nowMs());
|
|
252
|
+
program.metadata = {
|
|
253
|
+
...program.metadata,
|
|
254
|
+
auto_disabled_at_ms: Math.trunc(this.#nowMs()),
|
|
255
|
+
auto_disabled_reason: heartbeatResult.status === "skipped" ? (heartbeatResult.reason ?? null) : null,
|
|
256
|
+
};
|
|
257
|
+
this.#heartbeatScheduler.unregister(this.#scheduleId(program.program_id));
|
|
258
|
+
eventMessage = `${eventMessage} (auto-disabled)`;
|
|
282
259
|
}
|
|
260
|
+
await this.#persist();
|
|
261
|
+
await this.#emitTickEvent({
|
|
262
|
+
ts_ms: nowMs,
|
|
263
|
+
program_id: program.program_id,
|
|
264
|
+
message: eventMessage,
|
|
265
|
+
status: eventStatus,
|
|
266
|
+
reason: eventReason,
|
|
267
|
+
program: this.#snapshot(program),
|
|
268
|
+
}).catch(() => {
|
|
269
|
+
// best effort only
|
|
270
|
+
});
|
|
271
|
+
return heartbeatResult;
|
|
283
272
|
}
|
|
284
273
|
async list(opts = {}) {
|
|
285
274
|
await this.#ensureLoaded();
|
|
@@ -322,6 +311,7 @@ export class HeartbeatProgramRegistry {
|
|
|
322
311
|
? Math.max(0, Math.trunc(opts.everyMs))
|
|
323
312
|
: 15_000,
|
|
324
313
|
reason: opts.reason?.trim() || "scheduled",
|
|
314
|
+
wake_mode: normalizeWakeMode(opts.wakeMode),
|
|
325
315
|
target,
|
|
326
316
|
metadata: sanitizeMetadata(opts.metadata),
|
|
327
317
|
created_at_ms: nowMs,
|
|
@@ -354,6 +344,9 @@ export class HeartbeatProgramRegistry {
|
|
|
354
344
|
if (typeof opts.reason === "string") {
|
|
355
345
|
program.reason = opts.reason.trim() || "scheduled";
|
|
356
346
|
}
|
|
347
|
+
if (typeof opts.wakeMode === "string") {
|
|
348
|
+
program.wake_mode = normalizeWakeMode(opts.wakeMode);
|
|
349
|
+
}
|
|
357
350
|
if (typeof opts.enabled === "boolean") {
|
|
358
351
|
program.enabled = opts.enabled;
|
|
359
352
|
}
|
|
@@ -49,37 +49,61 @@ function toMs(value, fallback, min) {
|
|
|
49
49
|
}
|
|
50
50
|
return Math.max(min, Math.trunc(value));
|
|
51
51
|
}
|
|
52
|
+
function shouldRetry(result) {
|
|
53
|
+
if (result.status === "failed") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return result.status === "skipped" && result.reason === "requests-in-flight";
|
|
57
|
+
}
|
|
52
58
|
export class ActivityHeartbeatScheduler {
|
|
53
59
|
#states = new Map();
|
|
54
60
|
#nowMs;
|
|
55
61
|
#defaultCoalesceMs;
|
|
56
62
|
#retryMs;
|
|
57
63
|
#minIntervalMs;
|
|
64
|
+
#wakeTimerToken = 0;
|
|
58
65
|
constructor(opts = {}) {
|
|
59
66
|
this.#nowMs = opts.nowMs ?? defaultNowMs;
|
|
60
67
|
this.#defaultCoalesceMs = Math.max(0, Math.trunc(opts.defaultCoalesceMs ?? DEFAULT_COALESCE_MS));
|
|
61
68
|
this.#retryMs = Math.max(100, Math.trunc(opts.retryMs ?? DEFAULT_RETRY_MS));
|
|
62
69
|
this.#minIntervalMs = Math.max(100, Math.trunc(opts.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS));
|
|
63
70
|
}
|
|
71
|
+
#isCurrentState(state) {
|
|
72
|
+
if (state.disposed) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return this.#states.get(state.activityId) === state;
|
|
76
|
+
}
|
|
77
|
+
#normalizeDelayMs(coalesceMs) {
|
|
78
|
+
if (!Number.isFinite(coalesceMs)) {
|
|
79
|
+
return this.#defaultCoalesceMs;
|
|
80
|
+
}
|
|
81
|
+
return Math.max(0, Math.trunc(coalesceMs));
|
|
82
|
+
}
|
|
64
83
|
#clearWakeTimer(state) {
|
|
65
84
|
if (state.wakeTimer) {
|
|
66
|
-
clearTimeout(state.wakeTimer);
|
|
67
|
-
state.wakeTimer = null;
|
|
85
|
+
clearTimeout(state.wakeTimer.handle);
|
|
68
86
|
}
|
|
69
|
-
state.
|
|
70
|
-
state.wakeKind = null;
|
|
87
|
+
state.wakeTimer = null;
|
|
71
88
|
}
|
|
72
89
|
#disposeState(state) {
|
|
90
|
+
if (state.disposed) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
state.disposed = true;
|
|
73
94
|
if (state.intervalTimer) {
|
|
74
95
|
clearInterval(state.intervalTimer);
|
|
75
96
|
state.intervalTimer = null;
|
|
76
97
|
}
|
|
77
98
|
this.#clearWakeTimer(state);
|
|
78
99
|
state.pendingWake = null;
|
|
79
|
-
state.running = false;
|
|
80
100
|
state.scheduled = false;
|
|
101
|
+
state.running = false;
|
|
81
102
|
}
|
|
82
103
|
#queuePendingWakeReason(state, reason) {
|
|
104
|
+
if (!this.#isCurrentState(state)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
83
107
|
const normalized = normalizeReason(reason);
|
|
84
108
|
const next = {
|
|
85
109
|
reason: normalized,
|
|
@@ -98,73 +122,90 @@ export class ActivityHeartbeatScheduler {
|
|
|
98
122
|
state.pendingWake = next;
|
|
99
123
|
}
|
|
100
124
|
}
|
|
101
|
-
#
|
|
102
|
-
|
|
125
|
+
#scheduleWake(state, coalesceMs, kind = "normal") {
|
|
126
|
+
if (!this.#isCurrentState(state)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const delay = this.#normalizeDelayMs(coalesceMs);
|
|
103
130
|
const dueAt = this.#nowMs() + delay;
|
|
104
|
-
|
|
131
|
+
const activeTimer = state.wakeTimer;
|
|
132
|
+
if (activeTimer) {
|
|
105
133
|
// Retry cooldown should remain in force.
|
|
106
|
-
if (
|
|
134
|
+
if (activeTimer.kind === "retry") {
|
|
107
135
|
return;
|
|
108
136
|
}
|
|
109
|
-
if (
|
|
137
|
+
if (activeTimer.dueAt <= dueAt) {
|
|
110
138
|
return;
|
|
111
139
|
}
|
|
112
140
|
this.#clearWakeTimer(state);
|
|
113
141
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
const timerToken = ++this.#wakeTimerToken;
|
|
143
|
+
const handle = setTimeout(() => {
|
|
144
|
+
void this.#flushWake(state, {
|
|
145
|
+
timerToken,
|
|
146
|
+
delay,
|
|
147
|
+
kind,
|
|
148
|
+
});
|
|
118
149
|
}, delay);
|
|
119
|
-
|
|
150
|
+
handle.unref?.();
|
|
151
|
+
state.wakeTimer = {
|
|
152
|
+
handle,
|
|
153
|
+
dueAt,
|
|
154
|
+
kind,
|
|
155
|
+
token: timerToken,
|
|
156
|
+
};
|
|
120
157
|
}
|
|
121
|
-
async #
|
|
122
|
-
const state = this.#states.get(activityId);
|
|
123
|
-
if (!state) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
this.#clearWakeTimer(state);
|
|
127
|
-
state.scheduled = false;
|
|
128
|
-
if (state.running) {
|
|
129
|
-
state.scheduled = true;
|
|
130
|
-
this.#schedule(state, delay, kind);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const reason = state.pendingWake?.reason;
|
|
134
|
-
state.pendingWake = null;
|
|
135
|
-
state.running = true;
|
|
136
|
-
let result;
|
|
158
|
+
async #invokeHandler(state, reason) {
|
|
137
159
|
const startedAt = this.#nowMs();
|
|
138
160
|
try {
|
|
139
|
-
result = await state.handler({
|
|
161
|
+
const result = await state.handler({
|
|
162
|
+
activityId: state.activityId,
|
|
163
|
+
reason,
|
|
164
|
+
});
|
|
140
165
|
if (result.status === "ran" && result.durationMs == null) {
|
|
141
|
-
|
|
166
|
+
return {
|
|
142
167
|
status: "ran",
|
|
143
168
|
durationMs: Math.max(0, Math.trunc(this.#nowMs() - startedAt)),
|
|
144
169
|
};
|
|
145
170
|
}
|
|
171
|
+
return result;
|
|
146
172
|
}
|
|
147
173
|
catch (err) {
|
|
148
|
-
|
|
174
|
+
return {
|
|
149
175
|
status: "failed",
|
|
150
176
|
reason: err instanceof Error ? err.message : String(err),
|
|
151
177
|
};
|
|
152
178
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (this.#
|
|
179
|
+
}
|
|
180
|
+
async #flushWake(state, params) {
|
|
181
|
+
if (!this.#isCurrentState(state)) {
|
|
156
182
|
return;
|
|
157
183
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
184
|
+
const activeTimer = state.wakeTimer;
|
|
185
|
+
if (!activeTimer || activeTimer.token !== params.timerToken) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.#clearWakeTimer(state);
|
|
189
|
+
state.scheduled = false;
|
|
190
|
+
if (state.running) {
|
|
191
|
+
state.scheduled = true;
|
|
192
|
+
this.#scheduleWake(state, params.delay, params.kind);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const reason = state.pendingWake?.reason;
|
|
196
|
+
state.pendingWake = null;
|
|
197
|
+
state.running = true;
|
|
198
|
+
const result = await this.#invokeHandler(state, reason ?? undefined);
|
|
199
|
+
state.running = false;
|
|
200
|
+
if (!this.#isCurrentState(state)) {
|
|
201
|
+
return;
|
|
161
202
|
}
|
|
162
|
-
|
|
203
|
+
if (shouldRetry(result)) {
|
|
163
204
|
this.#queuePendingWakeReason(state, reason ?? "retry");
|
|
164
|
-
this.#
|
|
205
|
+
this.#scheduleWake(state, this.#retryMs, "retry");
|
|
165
206
|
}
|
|
166
207
|
if (state.pendingWake || state.scheduled) {
|
|
167
|
-
this.#
|
|
208
|
+
this.#scheduleWake(state, state.coalesceMs, "normal");
|
|
168
209
|
}
|
|
169
210
|
}
|
|
170
211
|
register(opts) {
|
|
@@ -174,9 +215,10 @@ export class ActivityHeartbeatScheduler {
|
|
|
174
215
|
this.#disposeState(existing);
|
|
175
216
|
this.#states.delete(activityId);
|
|
176
217
|
}
|
|
218
|
+
const hasInterval = Number.isFinite(opts.everyMs) && Math.trunc(opts.everyMs) > 0;
|
|
177
219
|
const state = {
|
|
178
220
|
activityId,
|
|
179
|
-
everyMs: toMs(opts.everyMs, this.#minIntervalMs, this.#minIntervalMs),
|
|
221
|
+
everyMs: hasInterval ? toMs(opts.everyMs, this.#minIntervalMs, this.#minIntervalMs) : 0,
|
|
180
222
|
coalesceMs: Math.max(0, Math.trunc(opts.coalesceMs ?? this.#defaultCoalesceMs)),
|
|
181
223
|
handler: opts.handler,
|
|
182
224
|
pendingWake: null,
|
|
@@ -184,13 +226,14 @@ export class ActivityHeartbeatScheduler {
|
|
|
184
226
|
running: false,
|
|
185
227
|
intervalTimer: null,
|
|
186
228
|
wakeTimer: null,
|
|
187
|
-
|
|
188
|
-
wakeKind: null,
|
|
229
|
+
disposed: false,
|
|
189
230
|
};
|
|
190
|
-
state.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
231
|
+
if (state.everyMs > 0) {
|
|
232
|
+
state.intervalTimer = setInterval(() => {
|
|
233
|
+
this.requestNow(activityId, { reason: "interval", coalesceMs: 0 });
|
|
234
|
+
}, state.everyMs);
|
|
235
|
+
state.intervalTimer.unref?.();
|
|
236
|
+
}
|
|
194
237
|
this.#states.set(activityId, state);
|
|
195
238
|
}
|
|
196
239
|
requestNow(activityIdRaw, opts) {
|
|
@@ -203,7 +246,7 @@ export class ActivityHeartbeatScheduler {
|
|
|
203
246
|
return false;
|
|
204
247
|
}
|
|
205
248
|
this.#queuePendingWakeReason(state, opts?.reason);
|
|
206
|
-
this.#
|
|
249
|
+
this.#scheduleWake(state, opts?.coalesceMs ?? state.coalesceMs, "normal");
|
|
207
250
|
return true;
|
|
208
251
|
}
|
|
209
252
|
unregister(activityIdRaw) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
export type { MuConfig, MuConfigPatch, MuConfigPresence } from "./config.js";
|
|
2
|
-
export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
|
|
3
1
|
export type { ControlPlaneActivityEvent, ControlPlaneActivityEventKind, ControlPlaneActivityMutationResult, ControlPlaneActivitySnapshot, ControlPlaneActivityStatus, ControlPlaneActivitySupervisorOpts, } from "./activity_supervisor.js";
|
|
4
2
|
export { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
5
|
-
export type {
|
|
6
|
-
export {
|
|
3
|
+
export type { MuConfig, MuConfigPatch, MuConfigPresence } from "./config.js";
|
|
4
|
+
export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
|
|
7
5
|
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle } from "./control_plane.js";
|
|
8
6
|
export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
|
|
9
|
-
export type {
|
|
7
|
+
export type { CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTarget, CronProgramTickEvent, CronProgramWakeMode, } from "./cron_programs.js";
|
|
8
|
+
export { CronProgramRegistry } from "./cron_programs.js";
|
|
9
|
+
export type { CronProgramSchedule as CronSchedule, CronProgramSchedule } from "./cron_schedule.js";
|
|
10
|
+
export { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedule.js";
|
|
11
|
+
export type { CronTimerRegistryOpts, CronTimerSnapshot } from "./cron_timer.js";
|
|
12
|
+
export { CronTimerRegistry } from "./cron_timer.js";
|
|
13
|
+
export type { HeartbeatProgramOperationResult, HeartbeatProgramRegistryOpts, HeartbeatProgramSnapshot, HeartbeatProgramTarget, HeartbeatProgramTickEvent, HeartbeatProgramWakeMode, } from "./heartbeat_programs.js";
|
|
14
|
+
export { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
15
|
+
export type { ActivityHeartbeatSchedulerOpts, HeartbeatRunResult, HeartbeatTickHandler, } from "./heartbeat_scheduler.js";
|
|
10
16
|
export { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
11
17
|
export type { ServerContext, ServerOptions, ServerWithControlPlane } from "./server.js";
|
|
12
18
|
export { createContext, createServer, createServerAsync } from "./server.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
|
|
2
1
|
export { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
3
|
-
export {
|
|
2
|
+
export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
|
|
4
3
|
export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
|
|
4
|
+
export { CronProgramRegistry } from "./cron_programs.js";
|
|
5
|
+
export { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedule.js";
|
|
6
|
+
export { CronTimerRegistry } from "./cron_timer.js";
|
|
7
|
+
export { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
5
8
|
export { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
6
9
|
export { createContext, createServer, createServerAsync } from "./server.js";
|
package/dist/run_supervisor.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { CommandRecord } from "@femtomc/mu-control-plane";
|
|
|
2
2
|
import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
3
3
|
export type ControlPlaneRunMode = "run_start" | "run_resume";
|
|
4
4
|
export type ControlPlaneRunStatus = "running" | "completed" | "failed" | "cancelled";
|
|
5
|
+
export type ControlPlaneRunWakeMode = "immediate" | "next_heartbeat";
|
|
5
6
|
export type ControlPlaneRunSnapshot = {
|
|
6
7
|
job_id: string;
|
|
7
8
|
mode: ControlPlaneRunMode;
|
|
@@ -95,6 +96,7 @@ export declare class ControlPlaneRunSupervisor {
|
|
|
95
96
|
jobId?: string | null;
|
|
96
97
|
rootIssueId?: string | null;
|
|
97
98
|
reason?: string | null;
|
|
99
|
+
wakeMode?: string | null;
|
|
98
100
|
}): ControlPlaneRunHeartbeatResult;
|
|
99
101
|
startFromCommand(command: CommandRecord): Promise<ControlPlaneRunSnapshot | null>;
|
|
100
102
|
stop(): Promise<void>;
|
package/dist/run_supervisor.js
CHANGED
|
@@ -45,6 +45,13 @@ function normalizeIssueId(value) {
|
|
|
45
45
|
}
|
|
46
46
|
return trimmed.toLowerCase();
|
|
47
47
|
}
|
|
48
|
+
function normalizeWakeMode(value) {
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
return "immediate";
|
|
51
|
+
}
|
|
52
|
+
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
53
|
+
return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
|
|
54
|
+
}
|
|
48
55
|
function pushBounded(lines, line, maxLines) {
|
|
49
56
|
lines.push(line);
|
|
50
57
|
if (lines.length <= maxLines) {
|
|
@@ -232,6 +239,7 @@ export class ControlPlaneRunSupervisor {
|
|
|
232
239
|
stderr_lines: [],
|
|
233
240
|
log_hints: new Set(),
|
|
234
241
|
interrupt_requested: false,
|
|
242
|
+
next_heartbeat_reason: null,
|
|
235
243
|
hard_kill_timer: null,
|
|
236
244
|
};
|
|
237
245
|
this.#jobsById.set(snapshot.job_id, job);
|
|
@@ -242,14 +250,22 @@ export class ControlPlaneRunSupervisor {
|
|
|
242
250
|
this.#heartbeatScheduler.register({
|
|
243
251
|
activityId: snapshot.job_id,
|
|
244
252
|
everyMs: this.#heartbeatIntervalMs,
|
|
245
|
-
handler: async () => {
|
|
253
|
+
handler: async ({ reason }) => {
|
|
246
254
|
if (job.snapshot.status !== "running") {
|
|
247
255
|
return { status: "skipped", reason: "not_running" };
|
|
248
256
|
}
|
|
257
|
+
const normalizedReason = reason?.trim();
|
|
258
|
+
const heartbeatReason = normalizedReason && normalizedReason.length > 0 && normalizedReason !== "interval"
|
|
259
|
+
? normalizedReason
|
|
260
|
+
: job.next_heartbeat_reason;
|
|
261
|
+
if (heartbeatReason) {
|
|
262
|
+
job.next_heartbeat_reason = null;
|
|
263
|
+
}
|
|
249
264
|
const elapsedSec = Math.max(0, Math.trunc((this.#nowMs() - job.snapshot.started_at_ms) / 1_000));
|
|
250
265
|
const root = job.snapshot.root_issue_id ?? job.snapshot.job_id;
|
|
251
266
|
const progress = job.snapshot.last_progress ? ` · ${job.snapshot.last_progress}` : "";
|
|
252
|
-
|
|
267
|
+
const reasonSuffix = heartbeatReason ? ` · wake=${heartbeatReason}` : "";
|
|
268
|
+
this.#emit("run_heartbeat", job, `⏱ ${root} running for ${elapsedSec}s${progress}${reasonSuffix}`);
|
|
253
269
|
return { status: "ran" };
|
|
254
270
|
},
|
|
255
271
|
});
|
|
@@ -288,7 +304,7 @@ export class ControlPlaneRunSupervisor {
|
|
|
288
304
|
throw new Error("run_start_prompt_required");
|
|
289
305
|
}
|
|
290
306
|
const maxSteps = toPositiveInt(opts.maxSteps, DEFAULT_MAX_STEPS);
|
|
291
|
-
const argv = ["mu", "
|
|
307
|
+
const argv = ["mu", "_run-direct", prompt, "--max-steps", String(maxSteps), "--raw-stream"];
|
|
292
308
|
return await this.#launch({
|
|
293
309
|
mode: "run_start",
|
|
294
310
|
prompt,
|
|
@@ -424,6 +440,15 @@ export class ControlPlaneRunSupervisor {
|
|
|
424
440
|
return { ok: false, reason: "not_running", run: this.#snapshot(job) };
|
|
425
441
|
}
|
|
426
442
|
const reason = opts.reason?.trim() || "manual";
|
|
443
|
+
const wakeMode = normalizeWakeMode(opts.wakeMode);
|
|
444
|
+
if (wakeMode === "next_heartbeat") {
|
|
445
|
+
job.next_heartbeat_reason = reason;
|
|
446
|
+
this.#touch(job);
|
|
447
|
+
return { ok: true, reason: null, run: this.#snapshot(job) };
|
|
448
|
+
}
|
|
449
|
+
if (reason !== "interval") {
|
|
450
|
+
job.next_heartbeat_reason = null;
|
|
451
|
+
}
|
|
427
452
|
this.#heartbeatScheduler.requestNow(job.snapshot.job_id, {
|
|
428
453
|
reason,
|
|
429
454
|
coalesceMs: 0,
|
package/dist/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { IssueStore } from "@femtomc/mu-issue";
|
|
|
6
6
|
import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
7
7
|
import { type MuConfig } from "./config.js";
|
|
8
8
|
import { type ControlPlaneConfig, type ControlPlaneHandle } from "./control_plane.js";
|
|
9
|
+
import { CronProgramRegistry } from "./cron_programs.js";
|
|
9
10
|
import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
10
11
|
import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
11
12
|
type ControlPlaneReloader = (opts: {
|
|
@@ -24,6 +25,8 @@ export type ServerOptions = {
|
|
|
24
25
|
activitySupervisor?: ControlPlaneActivitySupervisor;
|
|
25
26
|
controlPlaneReloader?: ControlPlaneReloader;
|
|
26
27
|
generationTelemetry?: GenerationTelemetryRecorder;
|
|
28
|
+
operatorWakeCoalesceMs?: number;
|
|
29
|
+
autoRunHeartbeatEveryMs?: number;
|
|
27
30
|
config?: MuConfig;
|
|
28
31
|
configReader?: ConfigReader;
|
|
29
32
|
configWriter?: ConfigWriter;
|
|
@@ -43,6 +46,7 @@ export declare function createServer(options?: ServerOptions): {
|
|
|
43
46
|
controlPlane: ControlPlaneHandle;
|
|
44
47
|
activitySupervisor: ControlPlaneActivitySupervisor;
|
|
45
48
|
heartbeatPrograms: HeartbeatProgramRegistry;
|
|
49
|
+
cronPrograms: CronProgramRegistry;
|
|
46
50
|
};
|
|
47
51
|
export type ServerWithControlPlane = {
|
|
48
52
|
serverConfig: ReturnType<typeof createServer>;
|