@femtomc/mu-server 26.2.120 → 26.3.2
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/api/heartbeats.js +90 -0
- package/dist/heartbeat_programs.d.ts +24 -0
- package/dist/heartbeat_programs.js +82 -1
- package/dist/server.js +66 -15
- package/dist/server_program_coordination.js +22 -9
- package/package.json +4 -4
package/dist/api/heartbeats.js
CHANGED
|
@@ -36,6 +36,39 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
36
36
|
if ("prompt" in body && typeof body.prompt !== "string" && body.prompt !== null) {
|
|
37
37
|
return Response.json({ error: "prompt must be string or null" }, { status: 400, headers });
|
|
38
38
|
}
|
|
39
|
+
const parseNullableString = (value) => {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (value === null)
|
|
43
|
+
return null;
|
|
44
|
+
if (typeof value === "string")
|
|
45
|
+
return value;
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const operatorProvider = parseNullableString(body.operator_provider);
|
|
49
|
+
if ("operator_provider" in body && operatorProvider === undefined) {
|
|
50
|
+
return Response.json({ error: "operator_provider must be string or null" }, { status: 400, headers });
|
|
51
|
+
}
|
|
52
|
+
const operatorModel = parseNullableString(body.operator_model);
|
|
53
|
+
if ("operator_model" in body && operatorModel === undefined) {
|
|
54
|
+
return Response.json({ error: "operator_model must be string or null" }, { status: 400, headers });
|
|
55
|
+
}
|
|
56
|
+
const operatorThinking = parseNullableString(body.operator_thinking);
|
|
57
|
+
if ("operator_thinking" in body && operatorThinking === undefined) {
|
|
58
|
+
return Response.json({ error: "operator_thinking must be string or null" }, { status: 400, headers });
|
|
59
|
+
}
|
|
60
|
+
const contextSessionId = parseNullableString(body.context_session_id);
|
|
61
|
+
if ("context_session_id" in body && contextSessionId === undefined) {
|
|
62
|
+
return Response.json({ error: "context_session_id must be string or null" }, { status: 400, headers });
|
|
63
|
+
}
|
|
64
|
+
const contextSessionFile = parseNullableString(body.context_session_file);
|
|
65
|
+
if ("context_session_file" in body && contextSessionFile === undefined) {
|
|
66
|
+
return Response.json({ error: "context_session_file must be string or null" }, { status: 400, headers });
|
|
67
|
+
}
|
|
68
|
+
const contextSessionDir = parseNullableString(body.context_session_dir);
|
|
69
|
+
if ("context_session_dir" in body && contextSessionDir === undefined) {
|
|
70
|
+
return Response.json({ error: "context_session_dir must be string or null" }, { status: 400, headers });
|
|
71
|
+
}
|
|
39
72
|
const prompt = typeof body.prompt === "string" ? body.prompt : body.prompt === null ? null : undefined;
|
|
40
73
|
const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
|
|
41
74
|
? Math.max(0, Math.trunc(body.every_ms))
|
|
@@ -52,6 +85,12 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
52
85
|
metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
|
|
53
86
|
? body.metadata
|
|
54
87
|
: undefined,
|
|
88
|
+
operatorProvider,
|
|
89
|
+
operatorModel,
|
|
90
|
+
operatorThinking,
|
|
91
|
+
contextSessionId,
|
|
92
|
+
contextSessionFile,
|
|
93
|
+
contextSessionDir,
|
|
55
94
|
});
|
|
56
95
|
return Response.json({ ok: true, program }, { status: 201, headers });
|
|
57
96
|
}
|
|
@@ -75,6 +114,15 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
75
114
|
return Response.json({ error: "program_id is required" }, { status: 400, headers });
|
|
76
115
|
}
|
|
77
116
|
try {
|
|
117
|
+
const parseNullableString = (value) => {
|
|
118
|
+
if (value === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (value === null)
|
|
121
|
+
return null;
|
|
122
|
+
if (typeof value === "string")
|
|
123
|
+
return value;
|
|
124
|
+
return undefined;
|
|
125
|
+
};
|
|
78
126
|
const updateOpts = {
|
|
79
127
|
programId,
|
|
80
128
|
title: typeof body.title === "string" ? body.title : undefined,
|
|
@@ -98,6 +146,48 @@ export async function heartbeatRoutes(request, url, deps, headers) {
|
|
|
98
146
|
return Response.json({ error: "prompt must be string or null" }, { status: 400, headers });
|
|
99
147
|
}
|
|
100
148
|
}
|
|
149
|
+
if ("operator_provider" in body) {
|
|
150
|
+
const value = parseNullableString(body.operator_provider);
|
|
151
|
+
if (value === undefined) {
|
|
152
|
+
return Response.json({ error: "operator_provider must be string or null" }, { status: 400, headers });
|
|
153
|
+
}
|
|
154
|
+
updateOpts.operatorProvider = value;
|
|
155
|
+
}
|
|
156
|
+
if ("operator_model" in body) {
|
|
157
|
+
const value = parseNullableString(body.operator_model);
|
|
158
|
+
if (value === undefined) {
|
|
159
|
+
return Response.json({ error: "operator_model must be string or null" }, { status: 400, headers });
|
|
160
|
+
}
|
|
161
|
+
updateOpts.operatorModel = value;
|
|
162
|
+
}
|
|
163
|
+
if ("operator_thinking" in body) {
|
|
164
|
+
const value = parseNullableString(body.operator_thinking);
|
|
165
|
+
if (value === undefined) {
|
|
166
|
+
return Response.json({ error: "operator_thinking must be string or null" }, { status: 400, headers });
|
|
167
|
+
}
|
|
168
|
+
updateOpts.operatorThinking = value;
|
|
169
|
+
}
|
|
170
|
+
if ("context_session_id" in body) {
|
|
171
|
+
const value = parseNullableString(body.context_session_id);
|
|
172
|
+
if (value === undefined) {
|
|
173
|
+
return Response.json({ error: "context_session_id must be string or null" }, { status: 400, headers });
|
|
174
|
+
}
|
|
175
|
+
updateOpts.contextSessionId = value;
|
|
176
|
+
}
|
|
177
|
+
if ("context_session_file" in body) {
|
|
178
|
+
const value = parseNullableString(body.context_session_file);
|
|
179
|
+
if (value === undefined) {
|
|
180
|
+
return Response.json({ error: "context_session_file must be string or null" }, { status: 400, headers });
|
|
181
|
+
}
|
|
182
|
+
updateOpts.contextSessionFile = value;
|
|
183
|
+
}
|
|
184
|
+
if ("context_session_dir" in body) {
|
|
185
|
+
const value = parseNullableString(body.context_session_dir);
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
return Response.json({ error: "context_session_dir must be string or null" }, { status: 400, headers });
|
|
188
|
+
}
|
|
189
|
+
updateOpts.contextSessionDir = value;
|
|
190
|
+
}
|
|
101
191
|
const result = await deps.heartbeatPrograms.update(updateOpts);
|
|
102
192
|
if (result.ok) {
|
|
103
193
|
return Response.json(result, { headers });
|
|
@@ -9,6 +9,12 @@ export type HeartbeatProgramSnapshot = {
|
|
|
9
9
|
every_ms: number;
|
|
10
10
|
reason: string;
|
|
11
11
|
metadata: Record<string, unknown>;
|
|
12
|
+
operator_provider: string | null;
|
|
13
|
+
operator_model: string | null;
|
|
14
|
+
operator_thinking: string | null;
|
|
15
|
+
context_session_id: string | null;
|
|
16
|
+
context_session_file: string | null;
|
|
17
|
+
context_session_dir: string | null;
|
|
12
18
|
created_at_ms: number;
|
|
13
19
|
updated_at_ms: number;
|
|
14
20
|
last_triggered_at_ms: number | null;
|
|
@@ -67,6 +73,12 @@ export type HeartbeatProgramRegistryOpts = {
|
|
|
67
73
|
prompt: string | null;
|
|
68
74
|
reason: string;
|
|
69
75
|
metadata: Record<string, unknown>;
|
|
76
|
+
operatorProvider: string | null;
|
|
77
|
+
operatorModel: string | null;
|
|
78
|
+
operatorThinking: string | null;
|
|
79
|
+
contextSessionId: string | null;
|
|
80
|
+
contextSessionFile: string | null;
|
|
81
|
+
contextSessionDir: string | null;
|
|
70
82
|
triggeredAtMs: number;
|
|
71
83
|
}) => Promise<HeartbeatProgramDispatchResult>;
|
|
72
84
|
onTickEvent?: (event: HeartbeatProgramTickEvent) => void | Promise<void>;
|
|
@@ -88,6 +100,12 @@ export declare class HeartbeatProgramRegistry {
|
|
|
88
100
|
reason?: string;
|
|
89
101
|
enabled?: boolean;
|
|
90
102
|
metadata?: Record<string, unknown>;
|
|
103
|
+
operatorProvider?: string | null;
|
|
104
|
+
operatorModel?: string | null;
|
|
105
|
+
operatorThinking?: string | null;
|
|
106
|
+
contextSessionId?: string | null;
|
|
107
|
+
contextSessionFile?: string | null;
|
|
108
|
+
contextSessionDir?: string | null;
|
|
91
109
|
}): Promise<HeartbeatProgramSnapshot>;
|
|
92
110
|
update(opts: {
|
|
93
111
|
programId: string;
|
|
@@ -97,6 +115,12 @@ export declare class HeartbeatProgramRegistry {
|
|
|
97
115
|
reason?: string;
|
|
98
116
|
enabled?: boolean;
|
|
99
117
|
metadata?: Record<string, unknown>;
|
|
118
|
+
operatorProvider?: string | null;
|
|
119
|
+
operatorModel?: string | null;
|
|
120
|
+
operatorThinking?: string | null;
|
|
121
|
+
contextSessionId?: string | null;
|
|
122
|
+
contextSessionFile?: string | null;
|
|
123
|
+
contextSessionDir?: string | null;
|
|
100
124
|
}): Promise<HeartbeatProgramOperationResult>;
|
|
101
125
|
remove(programId: string): Promise<HeartbeatProgramOperationResult>;
|
|
102
126
|
trigger(opts: {
|
|
@@ -19,6 +19,13 @@ function normalizePrompt(value) {
|
|
|
19
19
|
}
|
|
20
20
|
return value;
|
|
21
21
|
}
|
|
22
|
+
function normalizeOptionalString(value) {
|
|
23
|
+
if (typeof value !== "string") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
28
|
+
}
|
|
22
29
|
function normalizeProgram(row) {
|
|
23
30
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
24
31
|
return null;
|
|
@@ -43,7 +50,7 @@ function normalizeProgram(row) {
|
|
|
43
50
|
const lastResultRaw = typeof record.last_result === "string" ? record.last_result.trim().toLowerCase() : null;
|
|
44
51
|
const lastResult = lastResultRaw === "ok" || lastResultRaw === "coalesced" || lastResultRaw === "failed" ? lastResultRaw : null;
|
|
45
52
|
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : "scheduled";
|
|
46
|
-
|
|
53
|
+
const normalized = {
|
|
47
54
|
v: 1,
|
|
48
55
|
program_id: programId,
|
|
49
56
|
title,
|
|
@@ -52,12 +59,29 @@ function normalizeProgram(row) {
|
|
|
52
59
|
every_ms: everyMs,
|
|
53
60
|
reason,
|
|
54
61
|
metadata: sanitizeMetadata(record.metadata),
|
|
62
|
+
operator_provider: normalizeOptionalString(record.operator_provider),
|
|
63
|
+
operator_model: normalizeOptionalString(record.operator_model),
|
|
64
|
+
operator_thinking: normalizeOptionalString(record.operator_thinking),
|
|
65
|
+
context_session_id: normalizeOptionalString(record.context_session_id),
|
|
66
|
+
context_session_file: normalizeOptionalString(record.context_session_file),
|
|
67
|
+
context_session_dir: normalizeOptionalString(record.context_session_dir),
|
|
55
68
|
created_at_ms: createdAt,
|
|
56
69
|
updated_at_ms: updatedAt,
|
|
57
70
|
last_triggered_at_ms: lastTriggeredAt,
|
|
58
71
|
last_result: lastResult,
|
|
59
72
|
last_error: typeof record.last_error === "string" ? record.last_error : null,
|
|
60
73
|
};
|
|
74
|
+
const hasProvider = Boolean(normalized.operator_provider);
|
|
75
|
+
const hasModel = Boolean(normalized.operator_model);
|
|
76
|
+
if (hasProvider !== hasModel) {
|
|
77
|
+
normalized.operator_provider = null;
|
|
78
|
+
normalized.operator_model = null;
|
|
79
|
+
normalized.operator_thinking = null;
|
|
80
|
+
}
|
|
81
|
+
else if (!hasProvider) {
|
|
82
|
+
normalized.operator_thinking = null;
|
|
83
|
+
}
|
|
84
|
+
return normalized;
|
|
61
85
|
}
|
|
62
86
|
function sortPrograms(programs) {
|
|
63
87
|
return [...programs].sort((a, b) => {
|
|
@@ -67,6 +91,16 @@ function sortPrograms(programs) {
|
|
|
67
91
|
return a.program_id.localeCompare(b.program_id);
|
|
68
92
|
});
|
|
69
93
|
}
|
|
94
|
+
function validateOperatorRouting(program) {
|
|
95
|
+
const hasProvider = typeof program.operator_provider === "string" && program.operator_provider.length > 0;
|
|
96
|
+
const hasModel = typeof program.operator_model === "string" && program.operator_model.length > 0;
|
|
97
|
+
if (hasProvider !== hasModel) {
|
|
98
|
+
throw new Error("heartbeat_program_operator_provider_model_pair_required");
|
|
99
|
+
}
|
|
100
|
+
if (program.operator_thinking && !hasProvider) {
|
|
101
|
+
throw new Error("heartbeat_program_operator_thinking_requires_provider_model");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
export class HeartbeatProgramRegistry {
|
|
71
105
|
#store;
|
|
72
106
|
#heartbeatScheduler;
|
|
@@ -75,6 +109,7 @@ export class HeartbeatProgramRegistry {
|
|
|
75
109
|
#onLifecycleEvent;
|
|
76
110
|
#nowMs;
|
|
77
111
|
#programs = new Map();
|
|
112
|
+
#inFlightTicks = new Map();
|
|
78
113
|
#loaded = null;
|
|
79
114
|
constructor(opts) {
|
|
80
115
|
this.#heartbeatScheduler = opts.heartbeatScheduler;
|
|
@@ -160,6 +195,19 @@ export class HeartbeatProgramRegistry {
|
|
|
160
195
|
await this.#onLifecycleEvent(event);
|
|
161
196
|
}
|
|
162
197
|
async #tickProgram(programId, reason) {
|
|
198
|
+
const inFlight = this.#inFlightTicks.get(programId);
|
|
199
|
+
if (inFlight) {
|
|
200
|
+
return { status: "skipped", reason: "coalesced" };
|
|
201
|
+
}
|
|
202
|
+
const run = this.#tickProgramUnlocked(programId, reason).finally(() => {
|
|
203
|
+
if (this.#inFlightTicks.get(programId) === run) {
|
|
204
|
+
this.#inFlightTicks.delete(programId);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
this.#inFlightTicks.set(programId, run);
|
|
208
|
+
return await run;
|
|
209
|
+
}
|
|
210
|
+
async #tickProgramUnlocked(programId, reason) {
|
|
163
211
|
const program = this.#programs.get(programId);
|
|
164
212
|
if (!program) {
|
|
165
213
|
return { status: "skipped", reason: "not_found" };
|
|
@@ -182,6 +230,12 @@ export class HeartbeatProgramRegistry {
|
|
|
182
230
|
prompt: program.prompt,
|
|
183
231
|
reason: heartbeatReason,
|
|
184
232
|
metadata: { ...program.metadata },
|
|
233
|
+
operatorProvider: program.operator_provider,
|
|
234
|
+
operatorModel: program.operator_model,
|
|
235
|
+
operatorThinking: program.operator_thinking,
|
|
236
|
+
contextSessionId: program.context_session_id,
|
|
237
|
+
contextSessionFile: program.context_session_file,
|
|
238
|
+
contextSessionDir: program.context_session_dir,
|
|
185
239
|
triggeredAtMs: nowMs,
|
|
186
240
|
});
|
|
187
241
|
if (result.status === "ok") {
|
|
@@ -283,12 +337,19 @@ export class HeartbeatProgramRegistry {
|
|
|
283
337
|
every_ms: this.#normalizeEveryMs(typeof opts.everyMs === "number" && Number.isFinite(opts.everyMs) ? opts.everyMs : 15_000),
|
|
284
338
|
reason: opts.reason?.trim() || "scheduled",
|
|
285
339
|
metadata: sanitizeMetadata(opts.metadata),
|
|
340
|
+
operator_provider: normalizeOptionalString(opts.operatorProvider),
|
|
341
|
+
operator_model: normalizeOptionalString(opts.operatorModel),
|
|
342
|
+
operator_thinking: normalizeOptionalString(opts.operatorThinking),
|
|
343
|
+
context_session_id: normalizeOptionalString(opts.contextSessionId),
|
|
344
|
+
context_session_file: normalizeOptionalString(opts.contextSessionFile),
|
|
345
|
+
context_session_dir: normalizeOptionalString(opts.contextSessionDir),
|
|
286
346
|
created_at_ms: nowMs,
|
|
287
347
|
updated_at_ms: nowMs,
|
|
288
348
|
last_triggered_at_ms: null,
|
|
289
349
|
last_result: null,
|
|
290
350
|
last_error: null,
|
|
291
351
|
};
|
|
352
|
+
validateOperatorRouting(program);
|
|
292
353
|
this.#programs.set(program.program_id, program);
|
|
293
354
|
this.#applySchedule(program);
|
|
294
355
|
await this.#persist();
|
|
@@ -332,6 +393,25 @@ export class HeartbeatProgramRegistry {
|
|
|
332
393
|
if (opts.metadata) {
|
|
333
394
|
program.metadata = sanitizeMetadata(opts.metadata);
|
|
334
395
|
}
|
|
396
|
+
if ("operatorProvider" in opts) {
|
|
397
|
+
program.operator_provider = normalizeOptionalString(opts.operatorProvider);
|
|
398
|
+
}
|
|
399
|
+
if ("operatorModel" in opts) {
|
|
400
|
+
program.operator_model = normalizeOptionalString(opts.operatorModel);
|
|
401
|
+
}
|
|
402
|
+
if ("operatorThinking" in opts) {
|
|
403
|
+
program.operator_thinking = normalizeOptionalString(opts.operatorThinking);
|
|
404
|
+
}
|
|
405
|
+
if ("contextSessionId" in opts) {
|
|
406
|
+
program.context_session_id = normalizeOptionalString(opts.contextSessionId);
|
|
407
|
+
}
|
|
408
|
+
if ("contextSessionFile" in opts) {
|
|
409
|
+
program.context_session_file = normalizeOptionalString(opts.contextSessionFile);
|
|
410
|
+
}
|
|
411
|
+
if ("contextSessionDir" in opts) {
|
|
412
|
+
program.context_session_dir = normalizeOptionalString(opts.contextSessionDir);
|
|
413
|
+
}
|
|
414
|
+
validateOperatorRouting(program);
|
|
335
415
|
const nowMs = Math.trunc(this.#nowMs());
|
|
336
416
|
program.updated_at_ms = nowMs;
|
|
337
417
|
this.#applySchedule(program);
|
|
@@ -396,6 +476,7 @@ export class HeartbeatProgramRegistry {
|
|
|
396
476
|
for (const program of this.#programs.values()) {
|
|
397
477
|
this.#heartbeatScheduler.unregister(this.#scheduleId(program.program_id));
|
|
398
478
|
}
|
|
479
|
+
this.#inFlightTicks.clear();
|
|
399
480
|
this.#programs.clear();
|
|
400
481
|
}
|
|
401
482
|
}
|
package/dist/server.js
CHANGED
|
@@ -77,6 +77,20 @@ function extractWakeTurnReply(turnResult) {
|
|
|
77
77
|
const compact = presented.compact.trim();
|
|
78
78
|
return compact.length > 0 ? compact : null;
|
|
79
79
|
}
|
|
80
|
+
function extractWakeTurnFailureCode(turnReply) {
|
|
81
|
+
if (!turnReply) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const normalized = turnReply.trim();
|
|
85
|
+
if (!normalized.toLowerCase().startsWith("i could not complete that turn safely.")) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const codeMatch = normalized.match(/(?:^|\n)\s*Code:\s*([a-z0-9_]+)/i);
|
|
89
|
+
if (!codeMatch || typeof codeMatch[1] !== "string") {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return codeMatch[1].toLowerCase();
|
|
93
|
+
}
|
|
80
94
|
function buildWakeTurnIngressText(opts) {
|
|
81
95
|
const wakeSource = stringField(opts.payload, "wake_source") ?? "unknown";
|
|
82
96
|
const programId = stringField(opts.payload, "program_id") ?? "unknown";
|
|
@@ -195,16 +209,37 @@ function createServer(options = {}) {
|
|
|
195
209
|
message: opts.message,
|
|
196
210
|
payload: opts.payload,
|
|
197
211
|
});
|
|
212
|
+
const operatorProvider = stringField(opts.payload, "operator_provider");
|
|
213
|
+
const operatorModel = stringField(opts.payload, "operator_model");
|
|
214
|
+
const operatorThinking = stringField(opts.payload, "operator_thinking");
|
|
215
|
+
const contextSessionId = stringField(opts.payload, "context_session_id");
|
|
216
|
+
const contextSessionFile = stringField(opts.payload, "context_session_file");
|
|
217
|
+
const contextSessionDir = stringField(opts.payload, "context_session_dir");
|
|
218
|
+
const defaultHeartbeatSessionId = wakeSource === "heartbeat_program" && programId ? `heartbeat-program:${programId}` : null;
|
|
219
|
+
const operatorSessionId = contextSessionId ?? defaultHeartbeatSessionId;
|
|
220
|
+
const autonomousMetadata = {
|
|
221
|
+
wake_id: wakeId,
|
|
222
|
+
wake_source: wakeSource,
|
|
223
|
+
program_id: programId,
|
|
224
|
+
source_ts_ms: sourceTsMs,
|
|
225
|
+
};
|
|
226
|
+
if (operatorProvider)
|
|
227
|
+
autonomousMetadata.operator_provider = operatorProvider;
|
|
228
|
+
if (operatorModel)
|
|
229
|
+
autonomousMetadata.operator_model = operatorModel;
|
|
230
|
+
if (operatorThinking)
|
|
231
|
+
autonomousMetadata.operator_thinking = operatorThinking;
|
|
232
|
+
if (operatorSessionId)
|
|
233
|
+
autonomousMetadata.operator_session_id = operatorSessionId;
|
|
234
|
+
if (contextSessionFile)
|
|
235
|
+
autonomousMetadata.operator_session_file = contextSessionFile;
|
|
236
|
+
if (contextSessionDir)
|
|
237
|
+
autonomousMetadata.operator_session_dir = contextSessionDir;
|
|
198
238
|
const turnResult = await autonomousIngress({
|
|
199
239
|
text: ingressText,
|
|
200
240
|
repoRoot: context.repoRoot,
|
|
201
241
|
requestId: turnRequestId,
|
|
202
|
-
metadata:
|
|
203
|
-
wake_id: wakeId,
|
|
204
|
-
wake_source: wakeSource,
|
|
205
|
-
program_id: programId,
|
|
206
|
-
source_ts_ms: sourceTsMs,
|
|
207
|
-
},
|
|
242
|
+
metadata: autonomousMetadata,
|
|
208
243
|
});
|
|
209
244
|
if (turnResult.kind === "noop" || turnResult.kind === "invalid") {
|
|
210
245
|
decision = {
|
|
@@ -229,14 +264,27 @@ function createServer(options = {}) {
|
|
|
229
264
|
};
|
|
230
265
|
}
|
|
231
266
|
else {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
267
|
+
const failureCode = extractWakeTurnFailureCode(turnReply);
|
|
268
|
+
if (failureCode === "operator_busy") {
|
|
269
|
+
decision = {
|
|
270
|
+
outcome: "fallback",
|
|
271
|
+
reason: "operator_busy",
|
|
272
|
+
turnRequestId,
|
|
273
|
+
turnResultKind: turnResult.kind,
|
|
274
|
+
turnReply: null,
|
|
275
|
+
error: null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
decision = {
|
|
280
|
+
outcome: "triggered",
|
|
281
|
+
reason: "turn_invoked",
|
|
282
|
+
turnRequestId,
|
|
283
|
+
turnResultKind: turnResult.kind,
|
|
284
|
+
turnReply,
|
|
285
|
+
error: null,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
240
288
|
}
|
|
241
289
|
}
|
|
242
290
|
}
|
|
@@ -272,7 +320,7 @@ function createServer(options = {}) {
|
|
|
272
320
|
let notifyError = null;
|
|
273
321
|
let deliverySkippedReason = null;
|
|
274
322
|
if (!decision.turnReply) {
|
|
275
|
-
deliverySkippedReason = "no_turn_reply";
|
|
323
|
+
deliverySkippedReason = decision.reason === "operator_busy" ? "operator_busy" : "no_turn_reply";
|
|
276
324
|
}
|
|
277
325
|
else if (typeof controlPlaneProxy.notifyOperators !== "function") {
|
|
278
326
|
deliverySkippedReason = "notify_operators_unavailable";
|
|
@@ -346,6 +394,9 @@ function createServer(options = {}) {
|
|
|
346
394
|
delivery_error: notifyError,
|
|
347
395
|
},
|
|
348
396
|
});
|
|
397
|
+
if (decision.reason === "operator_busy") {
|
|
398
|
+
return { status: "coalesced", reason: decision.reason };
|
|
399
|
+
}
|
|
349
400
|
if (decision.outcome !== "triggered") {
|
|
350
401
|
return { status: "failed", reason: decision.reason };
|
|
351
402
|
}
|
|
@@ -12,18 +12,31 @@ export function createServerProgramCoordination(opts) {
|
|
|
12
12
|
heartbeatScheduler: opts.heartbeatScheduler,
|
|
13
13
|
dispatchWake: async (wakeOpts) => {
|
|
14
14
|
const prompt = wakeOpts.prompt && wakeOpts.prompt.trim().length > 0 ? wakeOpts.prompt : null;
|
|
15
|
+
const payload = {
|
|
16
|
+
wake_source: "heartbeat_program",
|
|
17
|
+
source_ts_ms: wakeOpts.triggeredAtMs,
|
|
18
|
+
program_id: wakeOpts.programId,
|
|
19
|
+
title: wakeOpts.title,
|
|
20
|
+
prompt,
|
|
21
|
+
reason: wakeOpts.reason,
|
|
22
|
+
metadata: wakeOpts.metadata,
|
|
23
|
+
};
|
|
24
|
+
if (wakeOpts.operatorProvider)
|
|
25
|
+
payload.operator_provider = wakeOpts.operatorProvider;
|
|
26
|
+
if (wakeOpts.operatorModel)
|
|
27
|
+
payload.operator_model = wakeOpts.operatorModel;
|
|
28
|
+
if (wakeOpts.operatorThinking)
|
|
29
|
+
payload.operator_thinking = wakeOpts.operatorThinking;
|
|
30
|
+
if (wakeOpts.contextSessionId)
|
|
31
|
+
payload.context_session_id = wakeOpts.contextSessionId;
|
|
32
|
+
if (wakeOpts.contextSessionFile)
|
|
33
|
+
payload.context_session_file = wakeOpts.contextSessionFile;
|
|
34
|
+
if (wakeOpts.contextSessionDir)
|
|
35
|
+
payload.context_session_dir = wakeOpts.contextSessionDir;
|
|
15
36
|
const wakeResult = await opts.emitOperatorWake({
|
|
16
37
|
dedupeKey: `heartbeat-program:${wakeOpts.programId}`,
|
|
17
38
|
message: prompt ?? `Heartbeat wake: ${wakeOpts.title}`,
|
|
18
|
-
payload
|
|
19
|
-
wake_source: "heartbeat_program",
|
|
20
|
-
source_ts_ms: wakeOpts.triggeredAtMs,
|
|
21
|
-
program_id: wakeOpts.programId,
|
|
22
|
-
title: wakeOpts.title,
|
|
23
|
-
prompt,
|
|
24
|
-
reason: wakeOpts.reason,
|
|
25
|
-
metadata: wakeOpts.metadata,
|
|
26
|
-
},
|
|
39
|
+
payload,
|
|
27
40
|
});
|
|
28
41
|
if (wakeResult.status === "coalesced") {
|
|
29
42
|
return { status: "coalesced", reason: wakeResult.reason };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2
|
|
3
|
+
"version": "26.3.2",
|
|
4
4
|
"description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"start": "bun run dist/cli.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@femtomc/mu-agent": "26.2
|
|
34
|
-
"@femtomc/mu-control-plane": "26.2
|
|
35
|
-
"@femtomc/mu-core": "26.2
|
|
33
|
+
"@femtomc/mu-agent": "26.3.2",
|
|
34
|
+
"@femtomc/mu-control-plane": "26.3.2",
|
|
35
|
+
"@femtomc/mu-core": "26.3.2"
|
|
36
36
|
}
|
|
37
37
|
}
|