@femtomc/mu-server 26.2.120 → 26.3.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.
@@ -75,6 +75,7 @@ export class HeartbeatProgramRegistry {
75
75
  #onLifecycleEvent;
76
76
  #nowMs;
77
77
  #programs = new Map();
78
+ #inFlightTicks = new Map();
78
79
  #loaded = null;
79
80
  constructor(opts) {
80
81
  this.#heartbeatScheduler = opts.heartbeatScheduler;
@@ -160,6 +161,19 @@ export class HeartbeatProgramRegistry {
160
161
  await this.#onLifecycleEvent(event);
161
162
  }
162
163
  async #tickProgram(programId, reason) {
164
+ const inFlight = this.#inFlightTicks.get(programId);
165
+ if (inFlight) {
166
+ return { status: "skipped", reason: "coalesced" };
167
+ }
168
+ const run = this.#tickProgramUnlocked(programId, reason).finally(() => {
169
+ if (this.#inFlightTicks.get(programId) === run) {
170
+ this.#inFlightTicks.delete(programId);
171
+ }
172
+ });
173
+ this.#inFlightTicks.set(programId, run);
174
+ return await run;
175
+ }
176
+ async #tickProgramUnlocked(programId, reason) {
163
177
  const program = this.#programs.get(programId);
164
178
  if (!program) {
165
179
  return { status: "skipped", reason: "not_found" };
@@ -396,6 +410,7 @@ export class HeartbeatProgramRegistry {
396
410
  for (const program of this.#programs.values()) {
397
411
  this.#heartbeatScheduler.unregister(this.#scheduleId(program.program_id));
398
412
  }
413
+ this.#inFlightTicks.clear();
399
414
  this.#programs.clear();
400
415
  }
401
416
  }
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";
@@ -229,14 +243,27 @@ function createServer(options = {}) {
229
243
  };
230
244
  }
231
245
  else {
232
- decision = {
233
- outcome: "triggered",
234
- reason: "turn_invoked",
235
- turnRequestId,
236
- turnResultKind: turnResult.kind,
237
- turnReply,
238
- error: null,
239
- };
246
+ const failureCode = extractWakeTurnFailureCode(turnReply);
247
+ if (failureCode === "operator_busy") {
248
+ decision = {
249
+ outcome: "fallback",
250
+ reason: "operator_busy",
251
+ turnRequestId,
252
+ turnResultKind: turnResult.kind,
253
+ turnReply: null,
254
+ error: null,
255
+ };
256
+ }
257
+ else {
258
+ decision = {
259
+ outcome: "triggered",
260
+ reason: "turn_invoked",
261
+ turnRequestId,
262
+ turnResultKind: turnResult.kind,
263
+ turnReply,
264
+ error: null,
265
+ };
266
+ }
240
267
  }
241
268
  }
242
269
  }
@@ -272,7 +299,7 @@ function createServer(options = {}) {
272
299
  let notifyError = null;
273
300
  let deliverySkippedReason = null;
274
301
  if (!decision.turnReply) {
275
- deliverySkippedReason = "no_turn_reply";
302
+ deliverySkippedReason = decision.reason === "operator_busy" ? "operator_busy" : "no_turn_reply";
276
303
  }
277
304
  else if (typeof controlPlaneProxy.notifyOperators !== "function") {
278
305
  deliverySkippedReason = "notify_operators_unavailable";
@@ -346,6 +373,9 @@ function createServer(options = {}) {
346
373
  delivery_error: notifyError,
347
374
  },
348
375
  });
376
+ if (decision.reason === "operator_busy") {
377
+ return { status: "coalesced", reason: decision.reason };
378
+ }
349
379
  if (decision.outcome !== "triggered") {
350
380
  return { status: "failed", reason: decision.reason };
351
381
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.120",
3
+ "version": "26.3.1",
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.120",
34
- "@femtomc/mu-control-plane": "26.2.120",
35
- "@femtomc/mu-core": "26.2.120"
33
+ "@femtomc/mu-agent": "26.3.1",
34
+ "@femtomc/mu-control-plane": "26.3.1",
35
+ "@femtomc/mu-core": "26.3.1"
36
36
  }
37
37
  }