@rikcodes/teamclaude 1.1.20-rik.8 → 1.1.20-rik.9
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/src/crash-log.js +36 -4
- package/src/tui.js +9 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rikcodes/teamclaude",
|
|
3
|
-
"version": "1.1.20-rik.
|
|
3
|
+
"version": "1.1.20-rik.9",
|
|
4
4
|
"description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/crash-log.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { appendFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
+
/** Did a write fail because whatever was reading has gone?
|
|
4
|
+
*
|
|
5
|
+
* Node reports this asynchronously when the stream is non-blocking, so it
|
|
6
|
+
* arrives as an event rather than at the call site and no `try` around the
|
|
7
|
+
* write can catch it. */
|
|
8
|
+
function isBrokenPipe(err) {
|
|
9
|
+
return err?.code === 'EPIPE' || err?.code === 'ERR_STREAM_DESTROYED';
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* Write a fatal error to `path` before the process dies.
|
|
5
14
|
*
|
|
@@ -11,16 +20,39 @@ import { appendFileSync } from 'node:fs';
|
|
|
11
20
|
* Handling these events replaces Node's own behaviour, so this must do what
|
|
12
21
|
* Node would: report and exit non-zero. Continuing after an uncaught exception
|
|
13
22
|
* would leave the proxy running on unknown state.
|
|
23
|
+
*
|
|
24
|
+
* A broken pipe is the one exception, and it is deliberate. The terminal going
|
|
25
|
+
* away says nothing about the proxy's state — every account, route and inflight
|
|
26
|
+
* request is exactly as it was — so ending the process punishes every routed
|
|
27
|
+
* session for a closed pane. It happened three times in two days here: the TUI
|
|
28
|
+
* writes to a non-blocking stdout, so the failure could not be caught where it
|
|
29
|
+
* was written, and each death also orphaned a sidecar on its port. Recorded
|
|
30
|
+
* once, because a stream dying is worth knowing, then survived. Once is enough:
|
|
31
|
+
* the stream stays dead, and a storm of them would say nothing new.
|
|
14
32
|
* @param {string} path
|
|
15
33
|
* @param {{ exit?: (code: number) => void, log?: { write: (s: string) => void } }} [opts]
|
|
16
34
|
*/
|
|
17
35
|
export function installCrashHandlers(path, { exit = process.exit, log = process.stderr } = {}) {
|
|
36
|
+
let brokenPipeRecorded = false;
|
|
37
|
+
// 0600: a stack can carry request context. A write failure (read-only home,
|
|
38
|
+
// full disk) must not mask the crash itself — stderr still gets the entry.
|
|
39
|
+
const record = (/** @type {string} */ entry) => {
|
|
40
|
+
try { appendFileSync(path, entry, { mode: 0o600 }); } catch { /* report to stderr regardless */ }
|
|
41
|
+
};
|
|
18
42
|
const report = (/** @type {string} */ kind) => (/** @type {any} */ err) => {
|
|
19
43
|
const stack = err?.stack || String(err);
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
44
|
+
// A bare "Error: write EPIPE" names neither the stream nor the call. The
|
|
45
|
+
// code and syscall are what turn the next one of these into a diagnosis
|
|
46
|
+
// rather than an afternoon of inference.
|
|
47
|
+
const detail = [err?.code, err?.syscall].filter(Boolean).join(' ');
|
|
48
|
+
const entry = `\n=== ${new Date().toISOString()} ${kind}${detail ? ` (${detail})` : ''} ===\n${stack}\n`;
|
|
49
|
+
if (isBrokenPipe(err)) {
|
|
50
|
+
if (!brokenPipeRecorded) { brokenPipeRecorded = true; record(entry); }
|
|
51
|
+
// Deliberately not written to `log`: that is very likely the stream that
|
|
52
|
+
// just failed, and would fail again.
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
record(entry);
|
|
24
56
|
log.write(entry);
|
|
25
57
|
exit(1);
|
|
26
58
|
};
|
package/src/tui.js
CHANGED
|
@@ -586,13 +586,16 @@ export class TUI {
|
|
|
586
586
|
this._setStdoutBlocking(true);
|
|
587
587
|
// Blocking again means a failed write throws here instead of arriving as
|
|
588
588
|
// an event, and a terminal that has already gone will fail. Restoring the
|
|
589
|
-
// screen is best-effort: there is nobody left to restore it for.
|
|
590
|
-
// listener outlives this write, so a late async failure is absorbed too.
|
|
589
|
+
// screen is best-effort: there is nobody left to restore it for.
|
|
591
590
|
try { process.stdout.write(`${ESC}?25h${ESC}?1049l`); } catch { /* terminal already gone */ }
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
591
|
+
// The error listener stays. Flipping back to blocking does not make writes
|
|
592
|
+
// ALREADY QUEUED synchronous, so a paint still in flight can fail after
|
|
593
|
+
// this point, and shutdown() runs well past it — stopping the prober, the
|
|
594
|
+
// warmer and the sidecar, then awaiting a state save. An earlier version
|
|
595
|
+
// removed the listener here while claiming it outlived the write; it did
|
|
596
|
+
// not, and the proxy died of an unhandled EPIPE in exactly that window. It
|
|
597
|
+
// only sets a flag, so leaving it attached for the rest of the process
|
|
598
|
+
// costs nothing.
|
|
596
599
|
try { process.stdin.setRawMode(false); } catch {}
|
|
597
600
|
process.stdin.pause();
|
|
598
601
|
}
|