@rubytech/create-maxy-code 0.1.614 → 0.1.616

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.
Files changed (104) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.d.ts +2 -0
  3. package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.d.ts.map +1 -0
  4. package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.js +291 -0
  5. package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.js.map +1 -0
  6. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts +13 -1
  7. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts.map +1 -1
  8. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js +127 -18
  9. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js.map +1 -1
  10. package/payload/platform/lib/aeo-llms-txt-writer/src/__tests__/write-llms-txt.test.ts +391 -0
  11. package/payload/platform/lib/aeo-llms-txt-writer/src/index.ts +156 -19
  12. package/payload/platform/lib/aeo-llms-txt-writer/vitest.config.ts +10 -0
  13. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +15 -1
  14. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -1
  15. package/payload/platform/lib/mcp-spawn-tee/dist/index.js +129 -7
  16. package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
  17. package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +227 -0
  18. package/payload/platform/lib/mcp-spawn-tee/src/index.ts +133 -6
  19. package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +129 -7
  20. package/payload/platform/plugins/admin/mcp/dist/index.js +11 -3
  21. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  22. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts +3 -0
  23. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts.map +1 -1
  24. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js +38 -20
  25. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js.map +1 -1
  26. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +6 -5
  27. package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +3 -0
  28. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +13 -0
  29. package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
  30. package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +129 -7
  31. package/payload/platform/plugins/aeo/mcp/dist/index.js +17 -10
  32. package/payload/platform/plugins/aeo/mcp/dist/index.js.map +1 -1
  33. package/payload/platform/plugins/browser/PLUGIN.md +1 -1
  34. package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +129 -7
  35. package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +129 -7
  36. package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +129 -7
  37. package/payload/platform/plugins/dispatch/lib/mcp-spawn-tee/index.js +129 -7
  38. package/payload/platform/plugins/docs/references/aeo.md +4 -3
  39. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  40. package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +129 -7
  41. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +129 -7
  42. package/payload/platform/plugins/google/lib/mcp-spawn-tee/index.js +129 -7
  43. package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +129 -7
  44. package/payload/platform/plugins/inventory/lib/mcp-spawn-tee/index.js +129 -7
  45. package/payload/platform/plugins/ledger/lib/mcp-spawn-tee/index.js +129 -7
  46. package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +129 -7
  47. package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +129 -7
  48. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +129 -7
  49. package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +129 -7
  50. package/payload/platform/plugins/scheduling/PLUGIN.md +3 -4
  51. package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +129 -7
  52. package/payload/platform/plugins/scheduling/mcp/dist/index.js +0 -6
  53. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  54. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js +65 -20
  55. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js.map +1 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js +20 -0
  57. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js.map +1 -1
  58. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +12 -4
  59. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
  60. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
  61. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
  62. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +39 -6
  63. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
  64. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +21 -6
  65. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -1
  66. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +26 -5
  67. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  68. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js +17 -51
  69. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js.map +1 -1
  70. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +6 -4
  71. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
  72. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +113 -1
  73. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
  74. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +8 -18
  75. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  76. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +14 -27
  77. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  78. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
  79. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +125 -56
  80. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
  81. package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +129 -7
  82. package/payload/platform/plugins/telegram/PLUGIN.md +2 -2
  83. package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +129 -7
  84. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +12 -5
  85. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -1
  86. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -1
  87. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +14 -6
  88. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -1
  89. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +1 -1
  90. package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +129 -7
  91. package/payload/platform/plugins/voice-mirror/lib/mcp-spawn-tee/index.js +129 -7
  92. package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +129 -7
  93. package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +129 -7
  94. package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +129 -7
  95. package/payload/platform/scripts/__tests__/vnc-cdp-liveness.test.sh +270 -19
  96. package/payload/platform/scripts/vnc.sh +174 -71
  97. package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
  98. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  99. package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.d.ts +80 -0
  100. package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.d.ts.map +1 -0
  101. package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.js +201 -0
  102. package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.js.map +1 -0
  103. package/payload/server/public/operator.html +1 -1
  104. package/payload/server/server.js +422 -251
@@ -25,10 +25,13 @@
25
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
26
26
  * the [mcp-helper] lifecycle lines.
27
27
  *
28
- * Lifecycle lines, correlation key `session=<id8> server=<name>` — unchanged:
28
+ * Lifecycle lines, correlation key `session=<id8> server=<name>`:
29
29
  * [mcp-helper] op=spawn ... pid= entry=
30
30
  * [mcp-helper] op=boot ... head=<first stderr bytes>
31
31
  * [mcp-helper] op=exit ... code= signal= lifetimeMs= stderr-tail=
32
+ * [mcp-helper] op=fatal ... kind=uncaught|rejection deferred=yes|no
33
+ * disposition=survived|exit error=
34
+ * preceded by an `op=fatal-stack` block carrying the full stack (Task 2787).
32
35
  *
33
36
  * Process model (Task 989): the shim IS the server's process. op=exit fires
34
37
  * from process.on("exit"), so it covers a normal exit, a non-zero exit, an
@@ -37,6 +40,17 @@
37
40
  * and leaves no op=exit line — the per-session stderr tail already on disk and
38
41
  * Claude Code's own transport-drop are the evidence for that death mode.
39
42
  *
43
+ * op=exit records THAT a fatal killed the process; it never records WHICH one,
44
+ * because Node's fatal printer writes beneath the tee (Task 2787). The op=fatal
45
+ * pair is what names the error.
46
+ *
47
+ * Task 2788 — a fatal no longer always kills. `disposition=` says what the guard
48
+ * did: the shim exits 1 only when it is the sole listener for the event AND no
49
+ * stdio transport is connected. A fatal on a connected transport is recorded
50
+ * and survived, because Claude Code's policy after a helper dies is one
51
+ * reconnect then stop, so killing a live server removes its whole tool surface
52
+ * for the rest of the session.
53
+ *
40
54
  * The shim never writes to fd 1 (stdout) — that is the JSON-RPC channel.
41
55
  */
42
56
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -114,14 +128,22 @@ function postToServerLog(suffix, level) {
114
128
  // Claude Code visibility, never re-teed), and a best-effort server.log mirror.
115
129
  // `suffix` is the line body after the tag and carries no newline (the
116
130
  // log-ingest route rejects newlines).
117
- function emitLifecycle(suffix, level) {
131
+ //
132
+ // Task 2787 — `mirror` is false for op=exit and op=fatal. Both fire while the
133
+ // loop is stopping, so the async fetch cannot flush: server.log held 2 op=exit
134
+ // lines against 160 op=spawn and 135 op=boot. The sync raw sink is the
135
+ // authoritative carrier for those two; posting them was a near-total loss
136
+ // dressed as a mirror. op=spawn/op=boot/op=error keep it — they fire while the
137
+ // loop is live and land.
138
+ function emitLifecycle(suffix, level, mirror = true) {
118
139
  const line = `[mcp-helper] ${suffix}\n`;
119
140
  appendSafe(rawSinkPath, line);
120
141
  try {
121
142
  rawStderrWrite(line);
122
143
  }
123
144
  catch { /* stderr closed */ }
124
- postToServerLog(suffix, level);
145
+ if (mirror)
146
+ postToServerLog(suffix, level);
125
147
  }
126
148
  if (!ENTRY) {
127
149
  emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason="no entry given (argv[2] missing)"`, "error");
@@ -165,6 +187,97 @@ function onSignal(sig, code) {
165
187
  process.on("SIGTERM", onSignal("SIGTERM", 143));
166
188
  process.on("SIGINT", onSignal("SIGINT", 130));
167
189
  process.on("SIGHUP", onSignal("SIGHUP", 129));
190
+ // Task 2787 — the fatal guard. Three measured facts drove it here rather than
191
+ // into the 26 plugin entries: Node's default fatal printer writes to fd 2
192
+ // BENEATH the JS `process.stderr.write` the tee replaces, so on both 18 Aug
193
+ // deaths the stack reached neither the raw sink nor Claude Code's own
194
+ // `Server stderr:` capture; the shim is the single seam every entry already
195
+ // loads through; and 26 copies would have to stay in step for ever.
196
+ //
197
+ // The stack is written through appendSafe and rawStderrWrite directly, never
198
+ // through teeWrite — the tee is the thing that was blind to it.
199
+ /** The three fields a fatal contributes. A rejection reason need not be an
200
+ * Error, in which case there is no stack and the stringified reason stands in
201
+ * for all three. */
202
+ function describeFatal(err) {
203
+ if (err instanceof Error) {
204
+ return { name: err.name, message: err.message, stack: err.stack ?? `${err.name}: ${err.message}` };
205
+ }
206
+ const s = String(err);
207
+ return { name: typeof err, message: s, stack: s };
208
+ }
209
+ /** Write a fatal's full stack as its own block, before the lifecycle line that
210
+ * summarises it. Multi-line by nature, which is why it is not a field on that
211
+ * line. Goes through appendSafe and the RAW writer — never teeWrite, which is
212
+ * the thing Node's own fatal printer writes beneath. */
213
+ function writeFatalStack(kind, stack) {
214
+ const block = `[mcp-helper] op=fatal-stack session=${SESSION_ID8} server=${SERVER_NAME} kind=${kind}\n${stack}\n`;
215
+ appendSafe(rawSinkPath, block);
216
+ try {
217
+ rawStderrWrite(block);
218
+ }
219
+ catch { /* stderr closed */ }
220
+ }
221
+ /** Task 2788 — has a stdio transport connected in this process?
222
+ *
223
+ * `server.connect(transport)` calls `StdioServerTransport.start()`, whose body
224
+ * is `this._stdin.on("data", …)`. So a stdin `data` listener exists if and only
225
+ * if a stdio transport has started, and `close()` removes it again — a fatal
226
+ * after the transport closed correctly reads as not connected.
227
+ *
228
+ * Read at the moment of the fatal rather than latched at first connect, so a
229
+ * transport that has since closed is not remembered as live. Nothing is
230
+ * patched: all 26 plugin entries construct a StdioServerTransport and none
231
+ * reads stdin itself, so this listener has exactly one source. The three
232
+ * channel services carry no stdio transport, so every fatal in them still
233
+ * exits — unchanged from before this task. */
234
+ function transportConnected() {
235
+ return process.stdin.listenerCount("data") > 0;
236
+ }
237
+ // Task 2788 — the disposition. `disposition=` names what THIS guard did with
238
+ // the process: `exit` means it called process.exit(1); `survived` means it
239
+ // returned and left the process running.
240
+ //
241
+ // Claude Code owns the spawn (Task 706: no ChildProcess handle; Task 989: the
242
+ // shim IS the server process) and its post-failure policy is one reconnect then
243
+ // stop — measured on 18 Aug, when the retry died the same way 2.6s in and the
244
+ // CLI never spawned again, leaving that session with `No such tool available`
245
+ // for the rest of its life. Nothing in this tree can change that policy, so not
246
+ // dying is the only mechanism available.
247
+ //
248
+ // The boundary is the connect, not a time window, because that is the fact that
249
+ // separates "a live server hit a bad request" from "this server never came up".
250
+ // Masking the second as the first would replace a visible outage with a silent
251
+ // one.
252
+ //
253
+ // * another listener present -> record and STAND DOWN, exactly as before. The
254
+ // email plugin installs its own pair (plugins/email/mcp/src/lib/setup-probe.ts)
255
+ // and deliberately survives errors it attributes to imapflow/nodemailer.
256
+ // The guard does not exit, so disposition=survived; if that handler then
257
+ // exits, the op=exit line that follows is the death record, and the census
258
+ // reads it as dark on its own terms.
259
+ // * sole listener, transport connected -> record and keep serving.
260
+ // * sole listener, no transport -> exit 1, which is Node's own default for an
261
+ // uncaught throw and (Node >= 15) for an unhandled rejection.
262
+ //
263
+ // The stack is written either way: surviving must not cost the diagnosis, which
264
+ // is what Task 2787 exists to preserve.
265
+ function onFatal(event, kind) {
266
+ return (err) => {
267
+ const sole = process.listenerCount(event) === 1;
268
+ const exiting = sole && !transportConnected();
269
+ const d = describeFatal(err);
270
+ // The stack first, so it is on disk before anything else can fail.
271
+ writeFatalStack(kind, d.stack);
272
+ emitLifecycle(`op=fatal session=${SESSION_ID8} server=${SERVER_NAME} kind=${kind} ` +
273
+ `deferred=${sole ? "no" : "yes"} disposition=${exiting ? "exit" : "survived"} ` +
274
+ `error=${JSON.stringify(JSON.stringify({ name: d.name, message: d.message }))}`, "error", false);
275
+ if (exiting)
276
+ process.exit(1);
277
+ };
278
+ }
279
+ process.on("uncaughtException", onFatal("uncaughtException", "uncaught"));
280
+ process.on("unhandledRejection", onFatal("unhandledRejection", "rejection"));
168
281
  // op=exit, emitted once from the process "exit" event. Sync-only — the loop is
169
282
  // stopping. When a catchable signal caused the exit, report code=— signal=<sig>
170
283
  // to match the prior wrapper's format; otherwise code=<exit code> signal=—.
@@ -177,7 +290,7 @@ process.on("exit", (code) => {
177
290
  const codeField = exitSignal ? "—" : String(code);
178
291
  const level = exitSignal || code !== 0 ? "error" : "info";
179
292
  emitLifecycle(`op=exit session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} ` +
180
- `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level);
293
+ `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level, false);
181
294
  });
182
295
  emitLifecycle(`op=spawn session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} entry=${ENTRY}`, "info");
183
296
  // Task 2356 — present argv as a direct `node <entry>` invocation before the
@@ -194,11 +307,20 @@ process.argv = [process.argv[0], ENTRY, ...process.argv.slice(3)];
194
307
  // Run the real MCP server in THIS process. Dynamic import() (not top-level
195
308
  // await — this file compiles to CommonJS) loads the ESM entry; a load-time
196
309
  // failure mirrors the old child spawn-error path (op=error, exit 127). The
197
- // op=exit handler is suppressed on this path so op=error stays the sole record.
310
+ // op=exit handler is suppressed on this path so op=error stays the sole
311
+ // lifecycle record.
312
+ //
313
+ // Task 2787 — this .catch() settles the rejection, so a throw during the
314
+ // entry's module EVALUATION never reaches the uncaughtException guard above.
315
+ // The stack is written here explicitly; without it a boot-time death leaves
316
+ // its message on the op=error line and nothing else — no type, no frames. The
317
+ // op=error line itself is unchanged: `describeFatal().message` is the same
318
+ // value the old inline expression produced.
198
319
  import((0, node_url_1.pathToFileURL)(ENTRY).href).catch((err) => {
199
- const msg = err instanceof Error ? err.message : String(err);
320
+ const d = describeFatal(err);
200
321
  exitEmitted = true;
201
- emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${msg}`)}`, "error");
322
+ writeFatalStack("import", d.stack);
323
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${d.message}`)}`, "error");
202
324
  process.exit(127);
203
325
  });
204
326
  //# sourceMappingURL=index.js.map
@@ -25,10 +25,13 @@
25
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
26
26
  * the [mcp-helper] lifecycle lines.
27
27
  *
28
- * Lifecycle lines, correlation key `session=<id8> server=<name>` — unchanged:
28
+ * Lifecycle lines, correlation key `session=<id8> server=<name>`:
29
29
  * [mcp-helper] op=spawn ... pid= entry=
30
30
  * [mcp-helper] op=boot ... head=<first stderr bytes>
31
31
  * [mcp-helper] op=exit ... code= signal= lifetimeMs= stderr-tail=
32
+ * [mcp-helper] op=fatal ... kind=uncaught|rejection deferred=yes|no
33
+ * disposition=survived|exit error=
34
+ * preceded by an `op=fatal-stack` block carrying the full stack (Task 2787).
32
35
  *
33
36
  * Process model (Task 989): the shim IS the server's process. op=exit fires
34
37
  * from process.on("exit"), so it covers a normal exit, a non-zero exit, an
@@ -37,6 +40,17 @@
37
40
  * and leaves no op=exit line — the per-session stderr tail already on disk and
38
41
  * Claude Code's own transport-drop are the evidence for that death mode.
39
42
  *
43
+ * op=exit records THAT a fatal killed the process; it never records WHICH one,
44
+ * because Node's fatal printer writes beneath the tee (Task 2787). The op=fatal
45
+ * pair is what names the error.
46
+ *
47
+ * Task 2788 — a fatal no longer always kills. `disposition=` says what the guard
48
+ * did: the shim exits 1 only when it is the sole listener for the event AND no
49
+ * stdio transport is connected. A fatal on a connected transport is recorded
50
+ * and survived, because Claude Code's policy after a helper dies is one
51
+ * reconnect then stop, so killing a live server removes its whole tool surface
52
+ * for the rest of the session.
53
+ *
40
54
  * The shim never writes to fd 1 (stdout) — that is the JSON-RPC channel.
41
55
  */
42
56
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -114,14 +128,22 @@ function postToServerLog(suffix, level) {
114
128
  // Claude Code visibility, never re-teed), and a best-effort server.log mirror.
115
129
  // `suffix` is the line body after the tag and carries no newline (the
116
130
  // log-ingest route rejects newlines).
117
- function emitLifecycle(suffix, level) {
131
+ //
132
+ // Task 2787 — `mirror` is false for op=exit and op=fatal. Both fire while the
133
+ // loop is stopping, so the async fetch cannot flush: server.log held 2 op=exit
134
+ // lines against 160 op=spawn and 135 op=boot. The sync raw sink is the
135
+ // authoritative carrier for those two; posting them was a near-total loss
136
+ // dressed as a mirror. op=spawn/op=boot/op=error keep it — they fire while the
137
+ // loop is live and land.
138
+ function emitLifecycle(suffix, level, mirror = true) {
118
139
  const line = `[mcp-helper] ${suffix}\n`;
119
140
  appendSafe(rawSinkPath, line);
120
141
  try {
121
142
  rawStderrWrite(line);
122
143
  }
123
144
  catch { /* stderr closed */ }
124
- postToServerLog(suffix, level);
145
+ if (mirror)
146
+ postToServerLog(suffix, level);
125
147
  }
126
148
  if (!ENTRY) {
127
149
  emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason="no entry given (argv[2] missing)"`, "error");
@@ -165,6 +187,97 @@ function onSignal(sig, code) {
165
187
  process.on("SIGTERM", onSignal("SIGTERM", 143));
166
188
  process.on("SIGINT", onSignal("SIGINT", 130));
167
189
  process.on("SIGHUP", onSignal("SIGHUP", 129));
190
+ // Task 2787 — the fatal guard. Three measured facts drove it here rather than
191
+ // into the 26 plugin entries: Node's default fatal printer writes to fd 2
192
+ // BENEATH the JS `process.stderr.write` the tee replaces, so on both 18 Aug
193
+ // deaths the stack reached neither the raw sink nor Claude Code's own
194
+ // `Server stderr:` capture; the shim is the single seam every entry already
195
+ // loads through; and 26 copies would have to stay in step for ever.
196
+ //
197
+ // The stack is written through appendSafe and rawStderrWrite directly, never
198
+ // through teeWrite — the tee is the thing that was blind to it.
199
+ /** The three fields a fatal contributes. A rejection reason need not be an
200
+ * Error, in which case there is no stack and the stringified reason stands in
201
+ * for all three. */
202
+ function describeFatal(err) {
203
+ if (err instanceof Error) {
204
+ return { name: err.name, message: err.message, stack: err.stack ?? `${err.name}: ${err.message}` };
205
+ }
206
+ const s = String(err);
207
+ return { name: typeof err, message: s, stack: s };
208
+ }
209
+ /** Write a fatal's full stack as its own block, before the lifecycle line that
210
+ * summarises it. Multi-line by nature, which is why it is not a field on that
211
+ * line. Goes through appendSafe and the RAW writer — never teeWrite, which is
212
+ * the thing Node's own fatal printer writes beneath. */
213
+ function writeFatalStack(kind, stack) {
214
+ const block = `[mcp-helper] op=fatal-stack session=${SESSION_ID8} server=${SERVER_NAME} kind=${kind}\n${stack}\n`;
215
+ appendSafe(rawSinkPath, block);
216
+ try {
217
+ rawStderrWrite(block);
218
+ }
219
+ catch { /* stderr closed */ }
220
+ }
221
+ /** Task 2788 — has a stdio transport connected in this process?
222
+ *
223
+ * `server.connect(transport)` calls `StdioServerTransport.start()`, whose body
224
+ * is `this._stdin.on("data", …)`. So a stdin `data` listener exists if and only
225
+ * if a stdio transport has started, and `close()` removes it again — a fatal
226
+ * after the transport closed correctly reads as not connected.
227
+ *
228
+ * Read at the moment of the fatal rather than latched at first connect, so a
229
+ * transport that has since closed is not remembered as live. Nothing is
230
+ * patched: all 26 plugin entries construct a StdioServerTransport and none
231
+ * reads stdin itself, so this listener has exactly one source. The three
232
+ * channel services carry no stdio transport, so every fatal in them still
233
+ * exits — unchanged from before this task. */
234
+ function transportConnected() {
235
+ return process.stdin.listenerCount("data") > 0;
236
+ }
237
+ // Task 2788 — the disposition. `disposition=` names what THIS guard did with
238
+ // the process: `exit` means it called process.exit(1); `survived` means it
239
+ // returned and left the process running.
240
+ //
241
+ // Claude Code owns the spawn (Task 706: no ChildProcess handle; Task 989: the
242
+ // shim IS the server process) and its post-failure policy is one reconnect then
243
+ // stop — measured on 18 Aug, when the retry died the same way 2.6s in and the
244
+ // CLI never spawned again, leaving that session with `No such tool available`
245
+ // for the rest of its life. Nothing in this tree can change that policy, so not
246
+ // dying is the only mechanism available.
247
+ //
248
+ // The boundary is the connect, not a time window, because that is the fact that
249
+ // separates "a live server hit a bad request" from "this server never came up".
250
+ // Masking the second as the first would replace a visible outage with a silent
251
+ // one.
252
+ //
253
+ // * another listener present -> record and STAND DOWN, exactly as before. The
254
+ // email plugin installs its own pair (plugins/email/mcp/src/lib/setup-probe.ts)
255
+ // and deliberately survives errors it attributes to imapflow/nodemailer.
256
+ // The guard does not exit, so disposition=survived; if that handler then
257
+ // exits, the op=exit line that follows is the death record, and the census
258
+ // reads it as dark on its own terms.
259
+ // * sole listener, transport connected -> record and keep serving.
260
+ // * sole listener, no transport -> exit 1, which is Node's own default for an
261
+ // uncaught throw and (Node >= 15) for an unhandled rejection.
262
+ //
263
+ // The stack is written either way: surviving must not cost the diagnosis, which
264
+ // is what Task 2787 exists to preserve.
265
+ function onFatal(event, kind) {
266
+ return (err) => {
267
+ const sole = process.listenerCount(event) === 1;
268
+ const exiting = sole && !transportConnected();
269
+ const d = describeFatal(err);
270
+ // The stack first, so it is on disk before anything else can fail.
271
+ writeFatalStack(kind, d.stack);
272
+ emitLifecycle(`op=fatal session=${SESSION_ID8} server=${SERVER_NAME} kind=${kind} ` +
273
+ `deferred=${sole ? "no" : "yes"} disposition=${exiting ? "exit" : "survived"} ` +
274
+ `error=${JSON.stringify(JSON.stringify({ name: d.name, message: d.message }))}`, "error", false);
275
+ if (exiting)
276
+ process.exit(1);
277
+ };
278
+ }
279
+ process.on("uncaughtException", onFatal("uncaughtException", "uncaught"));
280
+ process.on("unhandledRejection", onFatal("unhandledRejection", "rejection"));
168
281
  // op=exit, emitted once from the process "exit" event. Sync-only — the loop is
169
282
  // stopping. When a catchable signal caused the exit, report code=— signal=<sig>
170
283
  // to match the prior wrapper's format; otherwise code=<exit code> signal=—.
@@ -177,7 +290,7 @@ process.on("exit", (code) => {
177
290
  const codeField = exitSignal ? "—" : String(code);
178
291
  const level = exitSignal || code !== 0 ? "error" : "info";
179
292
  emitLifecycle(`op=exit session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} ` +
180
- `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level);
293
+ `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level, false);
181
294
  });
182
295
  emitLifecycle(`op=spawn session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} entry=${ENTRY}`, "info");
183
296
  // Task 2356 — present argv as a direct `node <entry>` invocation before the
@@ -194,11 +307,20 @@ process.argv = [process.argv[0], ENTRY, ...process.argv.slice(3)];
194
307
  // Run the real MCP server in THIS process. Dynamic import() (not top-level
195
308
  // await — this file compiles to CommonJS) loads the ESM entry; a load-time
196
309
  // failure mirrors the old child spawn-error path (op=error, exit 127). The
197
- // op=exit handler is suppressed on this path so op=error stays the sole record.
310
+ // op=exit handler is suppressed on this path so op=error stays the sole
311
+ // lifecycle record.
312
+ //
313
+ // Task 2787 — this .catch() settles the rejection, so a throw during the
314
+ // entry's module EVALUATION never reaches the uncaughtException guard above.
315
+ // The stack is written here explicitly; without it a boot-time death leaves
316
+ // its message on the op=error line and nothing else — no type, no frames. The
317
+ // op=error line itself is unchanged: `describeFatal().message` is the same
318
+ // value the old inline expression produced.
198
319
  import((0, node_url_1.pathToFileURL)(ENTRY).href).catch((err) => {
199
- const msg = err instanceof Error ? err.message : String(err);
320
+ const d = describeFatal(err);
200
321
  exitEmitted = true;
201
- emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${msg}`)}`, "error");
322
+ writeFatalStack("import", d.stack);
323
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${d.message}`)}`, "error");
202
324
  process.exit(127);
203
325
  });
204
326
  //# sourceMappingURL=index.js.map
@@ -296,7 +296,7 @@ Choosing a dispatch mode is **two questions, not one**: should this fire run *at
296
296
 
297
297
  1. **`action`** — nothing here needs a model. One pre-encoded MCP tool, no reasoning, no spawn.
298
298
  2. **`agentDispatch` with a `gate`** — a model is needed to compose or to judge, but only when there is something to act on. **This is the default for anything conditional.** The dispatcher calls the gate tool headlessly before spawning; `hasWork:false` skips the fire entirely.
299
- 3. **`agentDispatch` with no gate** judgement applies on *every* fire, whatever the state of the world: a daily briefing, a weekly digest. This is the exception, and it is stated as one. A recurring `agentDispatch` with no `gate` is refused unless it carries `gateWaived` naming why.
299
+ 3. There is no third shape. A recurring `agentDispatch` with no `gate` is refused. A routine that reaches the model on a fire with nothing to report is a routine that can send with nothing to report, so "judgement applies on every fire" is not an exception the write accepts. Express the trigger as a gate instead: the gate decides whether the fire happens, the prompt decides what to say once it does.
300
300
 
301
301
  When an agent-bearing event fires, the heartbeat hands the prompt to the destination's native channel: the channel spawns or resumes the deterministic admin session for that phone or chat, the agent runs the prompt with full admin tools, and its answer is delivered back over the account's paired socket. No operator session need be open.
302
302
 
@@ -325,8 +325,7 @@ Written as one model turn instead — "check whether it is signed yet and, if it
325
325
  - `channel` — `whatsapp` or `telegram`.
326
326
  - `destination` — the E.164 phone (WhatsApp) or chat id (Telegram) to run the turn for and reply to. It **must already be a registered admin or account-manager** on that channel; a destination that is not is rejected when the event is created. For WhatsApp the authoritative admin/manager lists are read from the socket-owning **house** account (never the event's own account), and the destination's registered account must match the account that created the schedule: an admin belongs to the house, a manager to its bound sub-account. The house/owner account may target any registered admin or manager; a sub-account is locked to its own manager(s). A refusal names the true reason — `not-registered` (in no house list) or `cross-account` (registered to a different account, naming both accounts).
327
327
  - `prompt` — the instruction the agent runs. Write it for the case where there **is** work: the gate has already decided that.
328
- - `gate` — `{ plugin, tool, args }`. The precondition, evaluated headlessly between the `nextRun` advance and the spawn. It must return `structuredContent {hasWork:boolean}`; the dispatcher reads that boolean and nothing else. A gate that errors, times out or returns no boolean **dispatches anyway**, so a broken check never silently mutes a routine. Requires a complete `agentDispatch`, is refused on an action-mode event, and is refused when any check names a type `scheduling-gate` does not probe.
329
- - `gateWaived` — a string reason, and the only way to create a recurring `agentDispatch` with no gate. Give the reason this routine has judgement to apply on every fire ("the morning briefing composes from scratch each day"). It is stored on the event and counted by the standing gate-classification check, so a deliberate exception stays readable and the fleet's drift back to ungated is a number someone can look at. A conditional sweep is not an exception; gate it.
328
+ - `gate` — `{ plugin, tool, args }`. The precondition, evaluated headlessly between the `nextRun` advance and the spawn. It must return `structuredContent {hasWork:boolean}`; the dispatcher reads that boolean and nothing else. A gate that errors, times out or returns no boolean **refuses the fire**: it has not shown there is anything to report, and nothing reaches the model until something to report is proven. The refused fire is recorded as `gate-unavailable` rather than `gated`, and `op=gate-census … gateUnavailable=<n>` plus one `op=gate-unavailable` line per routine is what stops a permanently broken gate reading as a routine that is merely quiet. Requires a complete `agentDispatch`, is refused on an action-mode event, and is refused when any check names a type `scheduling-gate` does not probe.
330
329
 
331
330
  **Gate check vocabulary.** A **`mail`** check answers whether a UID strictly above the stored watermark exists in the folder, optionally narrowed by `subjectPrefixes`. It is not "is there mail" — an empty answer on a full mailbox is the normal quiet state. The IMAP range `UID <n+1>:*` still yields the folder's last message when `n` is at or above the highest assigned UID (RFC 3501 §6.4.8), so the probe drops anything at or below the watermark before deciding; `[scheduling-gate] op=mail-probe … returned=<n> aboveWatermark=<n>` reports both counts, and `returned` exceeding `aboveWatermark` by exactly one is the steady state. A UIDVALIDITY change reports work, because the stored UID then names a different message. The watermark is keyed `<mailbox>|<folder>` with the folder lower-cased, because `INBOX` is case-insensitive by RFC 3501 §5.1: a binding that writes `"folder": "inbox"` and one that writes `"INBOX"` address one entry, so changing the case of an existing binding does not reset the position. The mailbox segment keeps its case, because a local part is case-sensitive. Keys already stored in mixed case fold on read, highest `uid` winning, and `[scheduling-gate] op=watermark-fold … kept=<key> droppedUid=<n>` names what the repair discarded.
332
331
 
@@ -359,7 +358,7 @@ Three lines record it. `[schedule-gate] op=mark-advanced eventId=… advanced=
359
358
 
360
359
  **Recurring only.** A one-time agent event is never gated — its row is already `due` by the time the dispatcher sees it, so a skip there would drop the routine for good — and it is therefore outside the refusal. `schedule-update` takes no waiver either: an existing routine keeps what it has until it is edited.
361
360
 
362
- An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`; the gate as `gatePlugin`, `gateTool`, `gateArgs`; the waiver as `gateWaived`. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account — unless that same phone is also on the house `adminPhones` list, in which case it scopes to the channel account, because the dispatch rides the house socket and so arrives from the house number. On Telegram the session runs on the house account.
361
+ An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`; the gate as `gatePlugin`, `gateTool`, `gateArgs`. Older rows may also carry a legacy `gateWaived` reason; it is read by the standing census and can no longer be written. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account — unless that same phone is also on the house `adminPhones` list, in which case it scopes to the channel account, because the dispatch rides the house socket and so arrives from the house number. On Telegram the session runs on the house account.
363
362
 
364
363
  A WhatsApp `agentDispatch` destination may therefore be an account manager, not only an admin phone — this is the supported way to send a manager a proactive or scheduled WhatsApp message. The reply is delivered over the house's paired line regardless of which sub-account the session is scoped to; socket ownership never restricts which number the dispatch can reach.
365
364
 
@@ -25,10 +25,13 @@
25
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
26
26
  * the [mcp-helper] lifecycle lines.
27
27
  *
28
- * Lifecycle lines, correlation key `session=<id8> server=<name>` — unchanged:
28
+ * Lifecycle lines, correlation key `session=<id8> server=<name>`:
29
29
  * [mcp-helper] op=spawn ... pid= entry=
30
30
  * [mcp-helper] op=boot ... head=<first stderr bytes>
31
31
  * [mcp-helper] op=exit ... code= signal= lifetimeMs= stderr-tail=
32
+ * [mcp-helper] op=fatal ... kind=uncaught|rejection deferred=yes|no
33
+ * disposition=survived|exit error=
34
+ * preceded by an `op=fatal-stack` block carrying the full stack (Task 2787).
32
35
  *
33
36
  * Process model (Task 989): the shim IS the server's process. op=exit fires
34
37
  * from process.on("exit"), so it covers a normal exit, a non-zero exit, an
@@ -37,6 +40,17 @@
37
40
  * and leaves no op=exit line — the per-session stderr tail already on disk and
38
41
  * Claude Code's own transport-drop are the evidence for that death mode.
39
42
  *
43
+ * op=exit records THAT a fatal killed the process; it never records WHICH one,
44
+ * because Node's fatal printer writes beneath the tee (Task 2787). The op=fatal
45
+ * pair is what names the error.
46
+ *
47
+ * Task 2788 — a fatal no longer always kills. `disposition=` says what the guard
48
+ * did: the shim exits 1 only when it is the sole listener for the event AND no
49
+ * stdio transport is connected. A fatal on a connected transport is recorded
50
+ * and survived, because Claude Code's policy after a helper dies is one
51
+ * reconnect then stop, so killing a live server removes its whole tool surface
52
+ * for the rest of the session.
53
+ *
40
54
  * The shim never writes to fd 1 (stdout) — that is the JSON-RPC channel.
41
55
  */
42
56
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -114,14 +128,22 @@ function postToServerLog(suffix, level) {
114
128
  // Claude Code visibility, never re-teed), and a best-effort server.log mirror.
115
129
  // `suffix` is the line body after the tag and carries no newline (the
116
130
  // log-ingest route rejects newlines).
117
- function emitLifecycle(suffix, level) {
131
+ //
132
+ // Task 2787 — `mirror` is false for op=exit and op=fatal. Both fire while the
133
+ // loop is stopping, so the async fetch cannot flush: server.log held 2 op=exit
134
+ // lines against 160 op=spawn and 135 op=boot. The sync raw sink is the
135
+ // authoritative carrier for those two; posting them was a near-total loss
136
+ // dressed as a mirror. op=spawn/op=boot/op=error keep it — they fire while the
137
+ // loop is live and land.
138
+ function emitLifecycle(suffix, level, mirror = true) {
118
139
  const line = `[mcp-helper] ${suffix}\n`;
119
140
  appendSafe(rawSinkPath, line);
120
141
  try {
121
142
  rawStderrWrite(line);
122
143
  }
123
144
  catch { /* stderr closed */ }
124
- postToServerLog(suffix, level);
145
+ if (mirror)
146
+ postToServerLog(suffix, level);
125
147
  }
126
148
  if (!ENTRY) {
127
149
  emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason="no entry given (argv[2] missing)"`, "error");
@@ -165,6 +187,97 @@ function onSignal(sig, code) {
165
187
  process.on("SIGTERM", onSignal("SIGTERM", 143));
166
188
  process.on("SIGINT", onSignal("SIGINT", 130));
167
189
  process.on("SIGHUP", onSignal("SIGHUP", 129));
190
+ // Task 2787 — the fatal guard. Three measured facts drove it here rather than
191
+ // into the 26 plugin entries: Node's default fatal printer writes to fd 2
192
+ // BENEATH the JS `process.stderr.write` the tee replaces, so on both 18 Aug
193
+ // deaths the stack reached neither the raw sink nor Claude Code's own
194
+ // `Server stderr:` capture; the shim is the single seam every entry already
195
+ // loads through; and 26 copies would have to stay in step for ever.
196
+ //
197
+ // The stack is written through appendSafe and rawStderrWrite directly, never
198
+ // through teeWrite — the tee is the thing that was blind to it.
199
+ /** The three fields a fatal contributes. A rejection reason need not be an
200
+ * Error, in which case there is no stack and the stringified reason stands in
201
+ * for all three. */
202
+ function describeFatal(err) {
203
+ if (err instanceof Error) {
204
+ return { name: err.name, message: err.message, stack: err.stack ?? `${err.name}: ${err.message}` };
205
+ }
206
+ const s = String(err);
207
+ return { name: typeof err, message: s, stack: s };
208
+ }
209
+ /** Write a fatal's full stack as its own block, before the lifecycle line that
210
+ * summarises it. Multi-line by nature, which is why it is not a field on that
211
+ * line. Goes through appendSafe and the RAW writer — never teeWrite, which is
212
+ * the thing Node's own fatal printer writes beneath. */
213
+ function writeFatalStack(kind, stack) {
214
+ const block = `[mcp-helper] op=fatal-stack session=${SESSION_ID8} server=${SERVER_NAME} kind=${kind}\n${stack}\n`;
215
+ appendSafe(rawSinkPath, block);
216
+ try {
217
+ rawStderrWrite(block);
218
+ }
219
+ catch { /* stderr closed */ }
220
+ }
221
+ /** Task 2788 — has a stdio transport connected in this process?
222
+ *
223
+ * `server.connect(transport)` calls `StdioServerTransport.start()`, whose body
224
+ * is `this._stdin.on("data", …)`. So a stdin `data` listener exists if and only
225
+ * if a stdio transport has started, and `close()` removes it again — a fatal
226
+ * after the transport closed correctly reads as not connected.
227
+ *
228
+ * Read at the moment of the fatal rather than latched at first connect, so a
229
+ * transport that has since closed is not remembered as live. Nothing is
230
+ * patched: all 26 plugin entries construct a StdioServerTransport and none
231
+ * reads stdin itself, so this listener has exactly one source. The three
232
+ * channel services carry no stdio transport, so every fatal in them still
233
+ * exits — unchanged from before this task. */
234
+ function transportConnected() {
235
+ return process.stdin.listenerCount("data") > 0;
236
+ }
237
+ // Task 2788 — the disposition. `disposition=` names what THIS guard did with
238
+ // the process: `exit` means it called process.exit(1); `survived` means it
239
+ // returned and left the process running.
240
+ //
241
+ // Claude Code owns the spawn (Task 706: no ChildProcess handle; Task 989: the
242
+ // shim IS the server process) and its post-failure policy is one reconnect then
243
+ // stop — measured on 18 Aug, when the retry died the same way 2.6s in and the
244
+ // CLI never spawned again, leaving that session with `No such tool available`
245
+ // for the rest of its life. Nothing in this tree can change that policy, so not
246
+ // dying is the only mechanism available.
247
+ //
248
+ // The boundary is the connect, not a time window, because that is the fact that
249
+ // separates "a live server hit a bad request" from "this server never came up".
250
+ // Masking the second as the first would replace a visible outage with a silent
251
+ // one.
252
+ //
253
+ // * another listener present -> record and STAND DOWN, exactly as before. The
254
+ // email plugin installs its own pair (plugins/email/mcp/src/lib/setup-probe.ts)
255
+ // and deliberately survives errors it attributes to imapflow/nodemailer.
256
+ // The guard does not exit, so disposition=survived; if that handler then
257
+ // exits, the op=exit line that follows is the death record, and the census
258
+ // reads it as dark on its own terms.
259
+ // * sole listener, transport connected -> record and keep serving.
260
+ // * sole listener, no transport -> exit 1, which is Node's own default for an
261
+ // uncaught throw and (Node >= 15) for an unhandled rejection.
262
+ //
263
+ // The stack is written either way: surviving must not cost the diagnosis, which
264
+ // is what Task 2787 exists to preserve.
265
+ function onFatal(event, kind) {
266
+ return (err) => {
267
+ const sole = process.listenerCount(event) === 1;
268
+ const exiting = sole && !transportConnected();
269
+ const d = describeFatal(err);
270
+ // The stack first, so it is on disk before anything else can fail.
271
+ writeFatalStack(kind, d.stack);
272
+ emitLifecycle(`op=fatal session=${SESSION_ID8} server=${SERVER_NAME} kind=${kind} ` +
273
+ `deferred=${sole ? "no" : "yes"} disposition=${exiting ? "exit" : "survived"} ` +
274
+ `error=${JSON.stringify(JSON.stringify({ name: d.name, message: d.message }))}`, "error", false);
275
+ if (exiting)
276
+ process.exit(1);
277
+ };
278
+ }
279
+ process.on("uncaughtException", onFatal("uncaughtException", "uncaught"));
280
+ process.on("unhandledRejection", onFatal("unhandledRejection", "rejection"));
168
281
  // op=exit, emitted once from the process "exit" event. Sync-only — the loop is
169
282
  // stopping. When a catchable signal caused the exit, report code=— signal=<sig>
170
283
  // to match the prior wrapper's format; otherwise code=<exit code> signal=—.
@@ -177,7 +290,7 @@ process.on("exit", (code) => {
177
290
  const codeField = exitSignal ? "—" : String(code);
178
291
  const level = exitSignal || code !== 0 ? "error" : "info";
179
292
  emitLifecycle(`op=exit session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} ` +
180
- `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level);
293
+ `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level, false);
181
294
  });
182
295
  emitLifecycle(`op=spawn session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} entry=${ENTRY}`, "info");
183
296
  // Task 2356 — present argv as a direct `node <entry>` invocation before the
@@ -194,11 +307,20 @@ process.argv = [process.argv[0], ENTRY, ...process.argv.slice(3)];
194
307
  // Run the real MCP server in THIS process. Dynamic import() (not top-level
195
308
  // await — this file compiles to CommonJS) loads the ESM entry; a load-time
196
309
  // failure mirrors the old child spawn-error path (op=error, exit 127). The
197
- // op=exit handler is suppressed on this path so op=error stays the sole record.
310
+ // op=exit handler is suppressed on this path so op=error stays the sole
311
+ // lifecycle record.
312
+ //
313
+ // Task 2787 — this .catch() settles the rejection, so a throw during the
314
+ // entry's module EVALUATION never reaches the uncaughtException guard above.
315
+ // The stack is written here explicitly; without it a boot-time death leaves
316
+ // its message on the op=error line and nothing else — no type, no frames. The
317
+ // op=error line itself is unchanged: `describeFatal().message` is the same
318
+ // value the old inline expression produced.
198
319
  import((0, node_url_1.pathToFileURL)(ENTRY).href).catch((err) => {
199
- const msg = err instanceof Error ? err.message : String(err);
320
+ const d = describeFatal(err);
200
321
  exitEmitted = true;
201
- emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${msg}`)}`, "error");
322
+ writeFatalStack("import", d.stack);
323
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${d.message}`)}`, "error");
202
324
  process.exit(127);
203
325
  });
204
326
  //# sourceMappingURL=index.js.map
@@ -73,9 +73,6 @@ const gateShape = {
73
73
  args: z.record(z.unknown()).optional().describe("Arguments passed to the gate tool verbatim"),
74
74
  };
75
75
  const GATE_DESCRIPTION = "Precondition: before this routine spawns an agent turn, the dispatcher calls this tool headlessly and reads structuredContent.hasWork. False skips the turn entirely; true dispatches as normal. A gate that errors, times out or returns no boolean dispatches anyway, so a broken gate never silently mutes the routine. Requires a complete agentDispatch and is refused on an action-mode event.";
76
- // Task 2383 — the declared exception, on the create path only. `schedule-update`
77
- // takes no waiver: an existing routine keeps what it has until it is edited.
78
- const GATE_WAIVED_DESCRIPTION = "The declared reason this recurring routine runs a model on every fire with no gate. A daily briefing or a weekly digest composes from scratch each time and has judgement to apply whether or not anything changed; a conditional sweep does not, and belongs behind a gate. Required when a recurring agentDispatch carries no gate — the write is refused without one. Stored on the event and counted by the standing gate-classification check, so every deliberate exception stays readable.";
79
76
  // ---------------------------------------------------------------------------
80
77
  // schedule-event
81
78
  // ---------------------------------------------------------------------------
@@ -105,7 +102,6 @@ eagerTool(server, "schedule-event", "Create a scheduled event or appointment. A
105
102
  botId: z.string().optional().describe("Telegram only — the bot that carries this routine. Omit to let the fire-time resolver pick whichever admin bot lists the chat id, which is correct for a single-admin-bot account. Name one to pin the carrier, so the routine cannot silently change bot when another bot's role changes."),
106
103
  }).optional().describe("Agent-bearing dispatch: when the event fires, spawns or resumes the admin channel session for `destination` and runs `prompt` with full admin tools, delivering the answer back over the channel. Mutually exclusive with `action`. Always records an event, never a meeting."),
107
104
  gate: z.object(gateShape).optional().describe(GATE_DESCRIPTION),
108
- gateWaived: z.string().optional().describe(GATE_WAIVED_DESCRIPTION),
109
105
  }, async (params) => {
110
106
  if (!accountId)
111
107
  return refuseNoAccount("schedule-event");
@@ -146,8 +142,6 @@ eagerTool(server, "schedule-event", "Create a scheduled event or appointment. A
146
142
  parts.push(`agent dispatch: ${params.agentDispatch.channel} → ${params.agentDispatch.destination}`);
147
143
  if (params.gate)
148
144
  parts.push(`gate: ${params.gate.plugin}/${params.gate.tool}`);
149
- if (params.gateWaived)
150
- parts.push(`gate waived: ${params.gateWaived}`);
151
145
  return { content: [{ type: "text", text: parts.join(", ") }] };
152
146
  }
153
147
  catch (err) {