@rubytech/create-maxy-code 0.1.615 → 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.
- package/package.json +1 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.d.ts +2 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.d.ts.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.js +291 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.js.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts +13 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js +127 -18
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js.map +1 -1
- package/payload/platform/lib/aeo-llms-txt-writer/src/__tests__/write-llms-txt.test.ts +391 -0
- package/payload/platform/lib/aeo-llms-txt-writer/src/index.ts +156 -19
- package/payload/platform/lib/aeo-llms-txt-writer/vitest.config.ts +10 -0
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +15 -1
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js +129 -7
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +227 -0
- package/payload/platform/lib/mcp-spawn-tee/src/index.ts +133 -6
- package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/admin/mcp/dist/index.js +11 -3
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts +3 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js +38 -20
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +6 -5
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +3 -0
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
- package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
- package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/aeo/mcp/dist/index.js +17 -10
- package/payload/platform/plugins/aeo/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/dispatch/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/docs/references/aeo.md +4 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/google/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/inventory/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/ledger/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/scheduling/PLUGIN.md +3 -4
- package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +0 -6
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js +65 -20
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js +20 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +12 -4
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +39 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +21 -6
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +26 -5
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js +17 -51
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +6 -4
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +113 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +8 -18
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +14 -27
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +125 -56
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
- package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/telegram/PLUGIN.md +2 -2
- package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +12 -5
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +14 -6
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -1
- package/payload/platform/plugins/telegram/skills/configure/SKILL.md +1 -1
- package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/voice-mirror/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.d.ts +80 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.js +201 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.js.map +1 -0
- package/payload/server/public/operator.html +1 -1
- package/payload/server/server.js +10 -2
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
* - `server.log` via the loopback log-ingest route — best-effort mirror of
|
|
25
25
|
* the [mcp-helper] lifecycle lines.
|
|
26
26
|
*
|
|
27
|
-
* Lifecycle lines, correlation key `session=<id8> server=<name
|
|
27
|
+
* Lifecycle lines, correlation key `session=<id8> server=<name>`:
|
|
28
28
|
* [mcp-helper] op=spawn ... pid= entry=
|
|
29
29
|
* [mcp-helper] op=boot ... head=<first stderr bytes>
|
|
30
30
|
* [mcp-helper] op=exit ... code= signal= lifetimeMs= stderr-tail=
|
|
31
|
+
* [mcp-helper] op=fatal ... kind=uncaught|rejection deferred=yes|no
|
|
32
|
+
* disposition=survived|exit error=
|
|
33
|
+
* preceded by an `op=fatal-stack` block carrying the full stack (Task 2787).
|
|
31
34
|
*
|
|
32
35
|
* Process model (Task 989): the shim IS the server's process. op=exit fires
|
|
33
36
|
* from process.on("exit"), so it covers a normal exit, a non-zero exit, an
|
|
@@ -36,6 +39,17 @@
|
|
|
36
39
|
* and leaves no op=exit line — the per-session stderr tail already on disk and
|
|
37
40
|
* Claude Code's own transport-drop are the evidence for that death mode.
|
|
38
41
|
*
|
|
42
|
+
* op=exit records THAT a fatal killed the process; it never records WHICH one,
|
|
43
|
+
* because Node's fatal printer writes beneath the tee (Task 2787). The op=fatal
|
|
44
|
+
* pair is what names the error.
|
|
45
|
+
*
|
|
46
|
+
* Task 2788 — a fatal no longer always kills. `disposition=` says what the guard
|
|
47
|
+
* did: the shim exits 1 only when it is the sole listener for the event AND no
|
|
48
|
+
* stdio transport is connected. A fatal on a connected transport is recorded
|
|
49
|
+
* and survived, because Claude Code's policy after a helper dies is one
|
|
50
|
+
* reconnect then stop, so killing a live server removes its whole tool surface
|
|
51
|
+
* for the rest of the session.
|
|
52
|
+
*
|
|
39
53
|
* The shim never writes to fd 1 (stdout) — that is the JSON-RPC channel.
|
|
40
54
|
*/
|
|
41
55
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG"}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
320
|
+
const d = describeFatal(err);
|
|
200
321
|
exitEmitted = true;
|
|
201
|
-
|
|
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AACA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,13 @@ function sessionLog(logDir: string, server: string, sessionId: string): string {
|
|
|
38
38
|
return readFileSync(join(logDir, `mcp-${server}-${sessionId}.log`), 'utf8')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// The `error=` field of an op=fatal line, still double-quoted. The line's own
|
|
42
|
+
// format is `error="{\"name\":…}"` — one JSON string whose content is JSON —
|
|
43
|
+
// so a message containing spaces cannot spill into a neighbouring field.
|
|
44
|
+
function errorField(line: string): string {
|
|
45
|
+
return /error=("(?:[^"\\]|\\.)*")/.exec(line)![1]
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
describe('mcp-spawn-tee in-process shim', () => {
|
|
42
49
|
it('runs the entry in the shim process — one node runtime, not two', () => {
|
|
43
50
|
const { stub, logDir } = makeStub('process.stdout.write(String(process.pid)); process.exit(0)')
|
|
@@ -188,4 +195,224 @@ describe('mcp-spawn-tee in-process shim', () => {
|
|
|
188
195
|
expect(r.status).toBe(0)
|
|
189
196
|
expect(sessionLog(logDir, 'stub', 'p1')).toContain('op=exit')
|
|
190
197
|
})
|
|
198
|
+
|
|
199
|
+
// Task 2787 — a fatal thrown after the entry connected is the 18 Aug death
|
|
200
|
+
// shape: the CLI sees only `Connection closed`, and before this guard the
|
|
201
|
+
// sink's stderr-tail ended at the last ordinary line with no stack anywhere.
|
|
202
|
+
it('records an uncaught throw with its stack, one op=fatal, then op=exit code=1', async () => {
|
|
203
|
+
const { stub, logDir } = makeStub(
|
|
204
|
+
'process.stderr.write("[stub] op=request tool=x\\n"); ' +
|
|
205
|
+
'setTimeout(() => { throw new TypeError("late boom") }, 50); ' +
|
|
206
|
+
'setInterval(() => {}, 1000)',
|
|
207
|
+
)
|
|
208
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
209
|
+
env: { ...process.env, SESSION_ID: 'fatal1', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
210
|
+
})
|
|
211
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
212
|
+
expect(exitCode).toBe(1)
|
|
213
|
+
const log = sessionLog(logDir, 'stub', 'fatal1')
|
|
214
|
+
const fatals = log.split('\n').filter((l) => l.includes(' op=fatal '))
|
|
215
|
+
expect(fatals).toHaveLength(1)
|
|
216
|
+
expect(fatals[0]).toContain('kind=uncaught')
|
|
217
|
+
expect(fatals[0]).toContain('deferred=no')
|
|
218
|
+
expect(fatals[0]).toContain('disposition=exit')
|
|
219
|
+
// error= is double-stringified, matching head= and stderr-tail= on the
|
|
220
|
+
// sibling lines: a message containing spaces must stay one field.
|
|
221
|
+
expect(JSON.parse(JSON.parse(errorField(fatals[0])))).toEqual({
|
|
222
|
+
name: 'TypeError',
|
|
223
|
+
message: 'late boom',
|
|
224
|
+
})
|
|
225
|
+
// The stack — the thing that reached neither sink on either 18 Aug death.
|
|
226
|
+
expect(log).toContain('TypeError: late boom')
|
|
227
|
+
expect(log).toMatch(/at .*stub-entry\.js/)
|
|
228
|
+
// op=exit follows the fatal, and still reads code=1.
|
|
229
|
+
const lines = log.split('\n')
|
|
230
|
+
expect(lines.findIndex((l) => l.includes('op=exit'))).toBeGreaterThan(
|
|
231
|
+
lines.findIndex((l) => l.includes(' op=fatal ')),
|
|
232
|
+
)
|
|
233
|
+
expect(lines.find((l) => l.includes('op=exit'))).toContain('code=1')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('records an unhandled rejection as kind=rejection, exactly once', async () => {
|
|
237
|
+
const { stub, logDir } = makeStub(
|
|
238
|
+
'setTimeout(() => { Promise.reject(new Error("no catch")) }, 50); setInterval(() => {}, 1000)',
|
|
239
|
+
)
|
|
240
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
241
|
+
env: { ...process.env, SESSION_ID: 'fatal2', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
242
|
+
})
|
|
243
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
244
|
+
expect(exitCode).toBe(1)
|
|
245
|
+
const log = sessionLog(logDir, 'stub', 'fatal2')
|
|
246
|
+
const fatals = log.split('\n').filter((l) => l.includes(' op=fatal '))
|
|
247
|
+
expect(fatals).toHaveLength(1)
|
|
248
|
+
expect(fatals[0]).toContain('kind=rejection')
|
|
249
|
+
expect(fatals[0]).toContain('deferred=no')
|
|
250
|
+
expect(fatals[0]).toContain('disposition=exit')
|
|
251
|
+
expect(JSON.parse(JSON.parse(errorField(fatals[0])))).toEqual({
|
|
252
|
+
name: 'Error',
|
|
253
|
+
message: 'no catch',
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('records a non-Error rejection reason', async () => {
|
|
258
|
+
const { stub, logDir } = makeStub(
|
|
259
|
+
'setTimeout(() => { Promise.reject("plain string reason") }, 50); setInterval(() => {}, 1000)',
|
|
260
|
+
)
|
|
261
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
262
|
+
env: { ...process.env, SESSION_ID: 'fatal3', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
263
|
+
})
|
|
264
|
+
await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
265
|
+
const fatal = sessionLog(logDir, 'stub', 'fatal3').split('\n').find((l) => l.includes(' op=fatal '))!
|
|
266
|
+
expect(fatal).toContain('kind=rejection')
|
|
267
|
+
expect(fatal).toContain('disposition=exit')
|
|
268
|
+
expect(fatal).toContain('plain string reason')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// Task 2788 — the disposition. A stdin 'data' listener is what
|
|
272
|
+
// StdioServerTransport.start() attaches, and is the shim's evidence that the
|
|
273
|
+
// entry came up; the stub stands in for it. Claude Code's policy after a
|
|
274
|
+
// helper dies is one reconnect then stop, so a fatal that kills a live server
|
|
275
|
+
// removes its whole tool surface for the rest of the session.
|
|
276
|
+
it('survives a fatal that arrives while a stdio transport is connected', async () => {
|
|
277
|
+
const { stub, logDir } = makeStub(
|
|
278
|
+
'process.stdin.on("data", () => {}); ' +
|
|
279
|
+
'process.stderr.write("[stub] connected\\n"); ' +
|
|
280
|
+
'setTimeout(() => { throw new TypeError("late boom") }, 50); ' +
|
|
281
|
+
'setTimeout(() => { process.stderr.write("[stub] still serving\\n"); process.exit(0) }, 300)',
|
|
282
|
+
)
|
|
283
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
284
|
+
env: { ...process.env, SESSION_ID: 'survive1', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
285
|
+
})
|
|
286
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
287
|
+
// Not killed by the throw: it ran on and exited on its own terms.
|
|
288
|
+
expect(exitCode).toBe(0)
|
|
289
|
+
const log = sessionLog(logDir, 'stub', 'survive1')
|
|
290
|
+
const fatals = log.split('\n').filter((l) => l.includes(' op=fatal '))
|
|
291
|
+
expect(fatals).toHaveLength(1)
|
|
292
|
+
expect(fatals[0]).toContain('disposition=survived')
|
|
293
|
+
// Surviving must not cost the diagnosis.
|
|
294
|
+
expect(log).toContain('TypeError: late boom')
|
|
295
|
+
// Proof the entry kept serving after the throw.
|
|
296
|
+
expect(log).toContain('[stub] still serving')
|
|
297
|
+
const exits = log.split('\n').filter((l) => l.includes('op=exit'))
|
|
298
|
+
expect(exits).toHaveLength(1)
|
|
299
|
+
expect(exits[0]).toContain('code=0')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// The outcome itself, not a proxy for it: a request sent AFTER the fatal is
|
|
303
|
+
// still answered on the stdio channel. This is the e852e7f4 shape, where the
|
|
304
|
+
// 07:35 and 07:36 turns instead hit `No such tool available`.
|
|
305
|
+
it('answers a request issued after the fatal', async () => {
|
|
306
|
+
const { stub, logDir } = makeStub(
|
|
307
|
+
'process.stdin.on("data", (b) => { if (String(b).includes("ping")) process.stdout.write("pong\\n") }); ' +
|
|
308
|
+
'process.stderr.write("[stub] connected\\n"); ' +
|
|
309
|
+
'setTimeout(() => { throw new TypeError("late boom") }, 50)',
|
|
310
|
+
)
|
|
311
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
312
|
+
env: { ...process.env, SESSION_ID: 'serve1', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
313
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
314
|
+
})
|
|
315
|
+
let out = ''
|
|
316
|
+
cp.stdout.on('data', (b: Buffer) => { out += String(b) })
|
|
317
|
+
// Well past the throw at 50ms.
|
|
318
|
+
await new Promise((res) => setTimeout(res, 400))
|
|
319
|
+
expect(cp.exitCode).toBeNull() // still alive
|
|
320
|
+
cp.stdin.write('ping\n')
|
|
321
|
+
await new Promise((res) => setTimeout(res, 300))
|
|
322
|
+
expect(out).toContain('pong')
|
|
323
|
+
cp.kill('SIGKILL')
|
|
324
|
+
const log = sessionLog(logDir, 'stub', 'serve1')
|
|
325
|
+
expect(log.split('\n').filter((l) => l.includes(' op=fatal '))[0]).toContain('disposition=survived')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('survives an unhandled rejection on a connected transport too', async () => {
|
|
329
|
+
const { stub, logDir } = makeStub(
|
|
330
|
+
'process.stdin.on("data", () => {}); ' +
|
|
331
|
+
'setTimeout(() => { Promise.reject(new Error("no catch")) }, 50); ' +
|
|
332
|
+
'setTimeout(() => { process.exit(0) }, 300)',
|
|
333
|
+
)
|
|
334
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
335
|
+
env: { ...process.env, SESSION_ID: 'survive2', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
336
|
+
})
|
|
337
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
338
|
+
expect(exitCode).toBe(0)
|
|
339
|
+
const fatal = sessionLog(logDir, 'stub', 'survive2').split('\n').find((l) => l.includes(' op=fatal '))!
|
|
340
|
+
expect(fatal).toContain('kind=rejection')
|
|
341
|
+
expect(fatal).toContain('disposition=survived')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// The boundary is the connect, not a time window: masking a server that never
|
|
345
|
+
// came up as a live one would replace a visible outage with a silent one.
|
|
346
|
+
it('still exits 1 for a fatal that arrives before any transport connected', async () => {
|
|
347
|
+
const { stub, logDir } = makeStub(
|
|
348
|
+
'process.stderr.write("[stub] booting\\n"); ' +
|
|
349
|
+
'setTimeout(() => { throw new TypeError("boot boom") }, 50); ' +
|
|
350
|
+
'setInterval(() => {}, 1000)',
|
|
351
|
+
)
|
|
352
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
353
|
+
env: { ...process.env, SESSION_ID: 'survive3', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
354
|
+
})
|
|
355
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
356
|
+
expect(exitCode).toBe(1)
|
|
357
|
+
const fatal = sessionLog(logDir, 'stub', 'survive3').split('\n').find((l) => l.includes(' op=fatal '))!
|
|
358
|
+
expect(fatal).toContain('disposition=exit')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// A transport that has closed is not serving, so the disposition is read at
|
|
362
|
+
// the moment of the fatal rather than latched at first connect.
|
|
363
|
+
it('exits 1 for a fatal that arrives after the transport closed', async () => {
|
|
364
|
+
const { stub, logDir } = makeStub(
|
|
365
|
+
'const onData = () => {}; process.stdin.on("data", onData); ' +
|
|
366
|
+
'setTimeout(() => { process.stdin.off("data", onData) }, 30); ' +
|
|
367
|
+
'setTimeout(() => { throw new TypeError("post-close boom") }, 80); ' +
|
|
368
|
+
'setInterval(() => {}, 1000)',
|
|
369
|
+
)
|
|
370
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
371
|
+
env: { ...process.env, SESSION_ID: 'survive4', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
372
|
+
})
|
|
373
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
374
|
+
expect(exitCode).toBe(1)
|
|
375
|
+
const fatal = sessionLog(logDir, 'stub', 'survive4').split('\n').find((l) => l.includes(' op=fatal '))!
|
|
376
|
+
expect(fatal).toContain('disposition=exit')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// A throw during module EVALUATION never reaches the uncaughtException
|
|
380
|
+
// guard — the import().catch() below settles the rejection first — so this
|
|
381
|
+
// path needs the stack written explicitly or a boot-time death leaves its
|
|
382
|
+
// message and nothing else.
|
|
383
|
+
it('records the stack for a throw during entry module evaluation', () => {
|
|
384
|
+
const { stub, logDir } = makeStub('throw new RangeError("boot-time explosion")')
|
|
385
|
+
const r = runSync(stub, { LOG_DIR: logDir, SESSION_ID: 'boot-throw', MCP_SPAWN_TEE_NAME: 'stub' })
|
|
386
|
+
expect(r.status).toBe(127)
|
|
387
|
+
const log = sessionLog(logDir, 'stub', 'boot-throw')
|
|
388
|
+
expect(log).toContain('op=fatal-stack')
|
|
389
|
+
expect(log).toContain('kind=import')
|
|
390
|
+
expect(log).toContain('RangeError: boot-time explosion')
|
|
391
|
+
expect(log).toMatch(/at .*stub-entry\.js/)
|
|
392
|
+
// The op=error line itself is unchanged — it is a format other readers grep.
|
|
393
|
+
expect(log).toContain('op=error')
|
|
394
|
+
expect(log).toContain('reason="import error: boot-time explosion"')
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// The email plugin installs its own uncaughtException handler that SURVIVES
|
|
398
|
+
// errors it attributes to imapflow/nodemailer (setup-probe.ts:127). The shim
|
|
399
|
+
// must record the fatal and then stand down, or that containment dies.
|
|
400
|
+
it('defers to an entry-owned fatal handler: records deferred=yes and does not exit', async () => {
|
|
401
|
+
const { stub, logDir } = makeStub(
|
|
402
|
+
'process.on("uncaughtException", () => { process.stderr.write("contained\\n") }); ' +
|
|
403
|
+
'setTimeout(() => { throw new Error("contained boom") }, 50); ' +
|
|
404
|
+
'setTimeout(() => { process.stderr.write("still alive\\n"); process.exit(0) }, 400)',
|
|
405
|
+
)
|
|
406
|
+
const cp = spawn(process.execPath, [shim, stub], {
|
|
407
|
+
env: { ...process.env, SESSION_ID: 'fatal4', PLATFORM_PORT: '', LOG_DIR: logDir, MCP_SPAWN_TEE_NAME: 'stub' },
|
|
408
|
+
})
|
|
409
|
+
const exitCode = await new Promise<number | null>((res) => cp.on('exit', (code) => res(code)))
|
|
410
|
+
expect(exitCode).toBe(0)
|
|
411
|
+
const log = sessionLog(logDir, 'stub', 'fatal4')
|
|
412
|
+
const fatal = log.split('\n').find((l) => l.includes(' op=fatal '))!
|
|
413
|
+
expect(fatal).toContain('deferred=yes')
|
|
414
|
+
expect(log).toContain('contained')
|
|
415
|
+
expect(log).toContain('still alive')
|
|
416
|
+
expect(log).toContain('code=0')
|
|
417
|
+
})
|
|
191
418
|
})
|